Merge remote-tracking branch 'origin/development' into regions/country-map-refetch
# Conflicts: # package.json
This commit is contained in:
commit
2b210b78f0
23 changed files with 1457 additions and 72 deletions
|
|
@ -42,6 +42,8 @@ and a change here can break other maps.
|
|||
`import … from '@map/components/dashboards/DashboardFilterChips.vue'`
|
||||
- **PinHint** — PinHint.vue — the conversion coach-mark (bottom-center of any map).
|
||||
`import … from '@map/components/hints/PinHint.vue'`
|
||||
- **MapInfo** — MapInfo.vue — a map's "what can this do for you?" button, one-time pointer and example popup.
|
||||
`import … from '@map/components/info/MapInfo.vue'`
|
||||
- **ApproxPinNote** — ApproxPinNote.vue — the ONE wording every map with people-group pins uses (APPROXPIN, 2026-09-26).
|
||||
`import … from '@map/components/legends/ApproxPinNote.vue'`
|
||||
- **LegendCollapseCaret** — LegendCollapseCaret.vue — SHARED (@map/components/legends/LegendCollapseCaret.vue).
|
||||
|
|
|
|||
280
internal/check-pane-config-reforward.mjs
Normal file
280
internal/check-pane-config-reforward.mjs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-pane-config-reforward — a composite's panes keep following the outer config for the whole
|
||||
* session, and a locale change never rewrites a filter into a display name.
|
||||
*
|
||||
* ★ THE BUG THIS GUARDS, measured by rega on development 92ca8b6e6 across FOUR composites
|
||||
* (regions-mfe, upg-100-mfe, country-posters-mfe, research-map-plus-grid). The outer element's
|
||||
* profile-config reached locale 'ar' by +1.5 s — attribute AND property, same node — while every
|
||||
* NESTED host carried NO locale key at boot and still none at +15 s. Text per composite at +15 s:
|
||||
* the chrome switched (3 Arabic / 0 Latin) and the panes did not (0/205, 0/190, 0/618, 0/43).
|
||||
* The cause is one line per composite: the pane's profile-config is written ONCE, in onMounted, so
|
||||
* the pane holds a JSON STRING and every later change stops at the composite. The leaf's own chain
|
||||
* is fully reactive and was never at fault.
|
||||
*
|
||||
* ★ IT RUNS THE SHIPPED HELPER WITH REAL VUE. usePaneConfig is imported, not re-typed, and driven
|
||||
* by real refs through a real watchEffect — the dependency-tracking hole this gate's case 3 covers
|
||||
* (a plain array leaves a LATER entry's getter untracked) is invisible to any test that fakes
|
||||
* reactivity, and it was found by studio only because their test used real Vue.
|
||||
*
|
||||
* ★ AND IT ASSERTS THE DOM, NOT THE RETURN VALUE. Every case reads the attribute back off the
|
||||
* element, because "the helper decided to write" and "the pane was told" are different claims and
|
||||
* only the second one is the fix.
|
||||
*
|
||||
* Exit 0 pass · 1 fail.
|
||||
*/
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
import { usePaneConfig } from '../library/composables/usePaneConfig.js'
|
||||
|
||||
let bad = 0
|
||||
const ok = (cond, what) => { if (!cond) bad++; console.log(` ${cond ? 'ok ' : 'FAIL'} ${what}`) }
|
||||
|
||||
/**
|
||||
* The smallest element that can hold attributes and answer contains(). Not a DOM shim in general —
|
||||
* only the surface the helper actually touches, so a change in what it touches breaks loudly here
|
||||
* rather than passing against a stub that pretends to be a browser.
|
||||
*/
|
||||
function makeEl(name = 'pane') {
|
||||
const attrs = new Map()
|
||||
return {
|
||||
name,
|
||||
isConnected: true,
|
||||
writes: 0,
|
||||
getAttribute: (k) => (attrs.has(k) ? attrs.get(k) : null),
|
||||
setAttribute(k, v) { attrs.set(k, String(v)); this.writes++ },
|
||||
}
|
||||
}
|
||||
const makeHost = (children) => ({ contains: (el) => children.includes(el) })
|
||||
|
||||
// ★ NULL-SAFE DELIBERATELY. When the re-forward is broken the attribute is absent, and a gate that
|
||||
// CRASHES on that reports "something went wrong somewhere" instead of naming the pane that stopped
|
||||
// following. Verified against the plain-array mutation: it turns a TypeError into a named FAIL.
|
||||
const cfgOf = (el) => {
|
||||
const raw = el.getAttribute('profile-config')
|
||||
if (!raw) return {}
|
||||
try { return JSON.parse(raw) } catch { return {} }
|
||||
}
|
||||
|
||||
// ── 1. THE MEASURED DEFECT. A locale change after mount must reach the pane.
|
||||
{
|
||||
const outer = ref({ locale: 'en', apiBaseUrl: '/api' })
|
||||
const el = makeEl()
|
||||
const host = makeHost([el])
|
||||
const paneConfig = usePaneConfig(() => host)
|
||||
// Mount-time write, exactly as the composite does it before registering.
|
||||
el.setAttribute('profile-config', JSON.stringify({ ...outer.value, profile: 'regions-list' }))
|
||||
paneConfig.register(el, () => ({ ...outer.value, profile: 'regions-list' }))
|
||||
await nextTick()
|
||||
const atMount = el.writes
|
||||
ok(cfgOf(el).locale === 'en', 'at mount the pane has the outer locale')
|
||||
|
||||
outer.value = { locale: 'ar', apiBaseUrl: '/api' }
|
||||
await nextTick()
|
||||
ok(cfgOf(el).locale === 'ar', 'after a live locale switch the pane has been told (this is the bug: it used to stay en)')
|
||||
ok(el.writes === atMount + 1, `exactly one re-write, not a loop (${el.writes - atMount})`)
|
||||
ok(cfgOf(el).profile === 'regions-list', "the pane's own keys survive the re-forward")
|
||||
paneConfig.dispose()
|
||||
}
|
||||
|
||||
// ── 1b. NO LOCALE KEY AT BOOT IS THE REAL BEFORE-STATE, not a stale one. rega measured the nested
|
||||
// hosts carrying no locale key at all, so the first re-forward is absent -> 'ar'.
|
||||
{
|
||||
const outer = ref({ apiBaseUrl: '/api' })
|
||||
const el = makeEl()
|
||||
const paneConfig = usePaneConfig(() => makeHost([el]))
|
||||
el.setAttribute('profile-config', JSON.stringify({ ...outer.value }))
|
||||
paneConfig.register(el, () => ({ ...outer.value }))
|
||||
await nextTick()
|
||||
ok(cfgOf(el).locale === undefined, 'the measured boot state: no locale key on the nested host')
|
||||
outer.value = { apiBaseUrl: '/api', locale: 'ar' }
|
||||
await nextTick()
|
||||
ok(cfgOf(el).locale === 'ar', 'an ABSENT locale key is still a change, so the write fires')
|
||||
paneConfig.dispose()
|
||||
}
|
||||
|
||||
// ── 2. NO WRITE WHEN NOTHING CHANGED. Setting profile-config re-parses it in the child and can
|
||||
// remount what it drives, so an unconditional write on every reactive tick is a rerender loop.
|
||||
{
|
||||
const outer = ref({ locale: 'en' })
|
||||
const unrelated = ref(0)
|
||||
const el = makeEl()
|
||||
const paneConfig = usePaneConfig(() => makeHost([el]))
|
||||
el.setAttribute('profile-config', JSON.stringify(outer.value))
|
||||
paneConfig.register(el, () => ({ ...outer.value, tick: unrelated.value && 0 }))
|
||||
await nextTick()
|
||||
const before = el.writes
|
||||
unrelated.value = 7 // reactive churn that does not change the config
|
||||
await nextTick()
|
||||
ok(el.writes === before, `an unchanged config writes nothing (${el.writes - before} writes)`)
|
||||
paneConfig.dispose()
|
||||
}
|
||||
|
||||
// ── 3. EVERY PANE FOLLOWS, NOT ONLY THE FIRST. ★ THE REGRESSION STUDIO FOUND. With a plain array
|
||||
// the watchEffect tracks only the getters reachable on its FIRST run, so pane 2's source is never a
|
||||
// dependency: changing it produced ZERO writes. That is this very bug, reappearing inside its own
|
||||
// fix, for any adopter whose panes read different sources.
|
||||
// It is also plugin-studio's load-bearing property, recorded at plugin-studio.vue:897: "EVERY
|
||||
// mounted pane, not only the one on screen: a kept-alive pane showed the old language when the
|
||||
// reader came back to it."
|
||||
{
|
||||
const a = ref('en')
|
||||
const b = ref('en')
|
||||
const elA = makeEl('a')
|
||||
const elB = makeEl('b')
|
||||
const paneConfig = usePaneConfig(() => makeHost([elA, elB]))
|
||||
paneConfig.register(elA, () => ({ locale: a.value }))
|
||||
paneConfig.register(elB, () => ({ locale: b.value }))
|
||||
await nextTick()
|
||||
b.value = 'ar' // ONLY the second pane's source
|
||||
await nextTick()
|
||||
ok(cfgOf(elB).locale === 'ar', 'the SECOND registered pane follows its own source (a plain array left this untracked)')
|
||||
ok(cfgOf(elA).locale === 'en', 'and the first pane is not disturbed by it')
|
||||
a.value = 'ro'
|
||||
await nextTick()
|
||||
ok(cfgOf(elA).locale === 'ro', 'the first pane still follows after the second has fired')
|
||||
paneConfig.dispose()
|
||||
}
|
||||
|
||||
// ── 4. A DETACHED PANE IS DROPPED, not written to. A pane column rebuild replaces the elements.
|
||||
{
|
||||
const outer = ref({ locale: 'en' })
|
||||
const kept = makeEl('kept')
|
||||
const gone = makeEl('gone')
|
||||
let children = [kept, gone]
|
||||
const paneConfig = usePaneConfig(() => makeHost(children))
|
||||
paneConfig.register(kept, () => ({ ...outer.value }))
|
||||
paneConfig.register(gone, () => ({ ...outer.value }))
|
||||
await nextTick()
|
||||
ok(paneConfig.count() === 2, 'both panes registered')
|
||||
children = [kept] // the column was rebuilt; `gone` is no longer under it
|
||||
gone.isConnected = false
|
||||
const goneWrites = gone.writes
|
||||
outer.value = { locale: 'ar' }
|
||||
await nextTick()
|
||||
ok(gone.writes === goneWrites, 'a detached pane is not written to')
|
||||
ok(paneConfig.count() === 1, `and it is PRUNED rather than re-tested forever (count ${paneConfig.count()})`)
|
||||
ok(cfgOf(kept).locale === 'ar', 'the surviving pane still follows')
|
||||
paneConfig.dispose()
|
||||
}
|
||||
|
||||
// ── 5. ONE PANE'S THROWING GETTER DOES NOT TAKE THE OTHERS DOWN. Inside a watchEffect an exception
|
||||
// stops every LATER entry in the same pass — which would silently freeze panes that are fine.
|
||||
{
|
||||
const outer = ref({ locale: 'en' })
|
||||
const boom = makeEl('boom')
|
||||
const fine = makeEl('fine')
|
||||
const paneConfig = usePaneConfig(() => makeHost([boom, fine]))
|
||||
const realError = console.error
|
||||
let errors = 0
|
||||
console.error = () => { errors++ }
|
||||
paneConfig.register(boom, () => { throw new Error('pane getter exploded') })
|
||||
paneConfig.register(fine, () => ({ ...outer.value }))
|
||||
await nextTick()
|
||||
outer.value = { locale: 'ar' }
|
||||
await nextTick()
|
||||
console.error = realError
|
||||
ok(cfgOf(fine).locale === 'ar', 'a pane registered AFTER a throwing one still follows the outer config')
|
||||
ok(errors >= 1, `and the throw is reported rather than swallowed (${errors})`)
|
||||
ok(errors <= 2, `but not once per reactive tick forever (${errors})`)
|
||||
paneConfig.dispose()
|
||||
}
|
||||
|
||||
// ── 6. registerAttrs: THE SECOND PANE SHAPE. people-groups-grid takes three separate attributes
|
||||
// rather than one JSON blob (research-map-plus-grid.vue:430-435). Same list, same effect, same
|
||||
// write-on-change — the point of generalising rather than letting the second adopter fork.
|
||||
{
|
||||
const linked = reactive({ region: '', religion: '', language: 'arb' })
|
||||
const grid = makeEl('grid')
|
||||
const paneConfig = usePaneConfig(() => makeHost([grid]))
|
||||
paneConfig.registerAttrs(grid, () => ({
|
||||
'wagf-region': linked.region,
|
||||
'religion-family': linked.religion,
|
||||
'primary-language': linked.language,
|
||||
}))
|
||||
await nextTick()
|
||||
ok(grid.getAttribute('primary-language') === 'arb', 'an attribute-configured pane is written at registration')
|
||||
const before = grid.writes
|
||||
linked.region = 'africa'
|
||||
await nextTick()
|
||||
ok(grid.getAttribute('wagf-region') === 'africa', 'a changed attribute is written')
|
||||
ok(grid.writes === before + 1, `ONLY the changed attribute is written, not all three (${grid.writes - before})`)
|
||||
ok(grid.getAttribute('religion-family') === '', 'an empty value is written as empty, never the string "undefined"')
|
||||
paneConfig.dispose()
|
||||
}
|
||||
|
||||
// ── 6b. A KEY THE GETTER LATER OMITS IS CLEARED, not left applied. A stale filter silently
|
||||
// narrowing the grid is the same class of lie as the frozen config this helper exists to fix.
|
||||
{
|
||||
const linked = reactive({ region: 'africa', present: true })
|
||||
const grid = makeEl('grid')
|
||||
const paneConfig = usePaneConfig(() => makeHost([grid]))
|
||||
paneConfig.registerAttrs(grid, () => (linked.present
|
||||
? { 'wagf-region': linked.region, 'religion-family': 'islam' }
|
||||
: { 'religion-family': 'islam' }))
|
||||
await nextTick()
|
||||
ok(grid.getAttribute('wagf-region') === 'africa', 'the filter is applied')
|
||||
linked.present = false
|
||||
await nextTick()
|
||||
ok(grid.getAttribute('wagf-region') === '', 'an omitted key CLEARS its attribute rather than leaving a stale filter')
|
||||
paneConfig.dispose()
|
||||
}
|
||||
|
||||
// ── 7. ★ LOCALE-INVARIANCE OF A FILTER VALUE (sr's property, SR-ADOPT-ANSWERS). A re-forward
|
||||
// triggered by a LOCALE change must produce the SAME primary-language string it produced before.
|
||||
// The hazard is real and general even though it does not bite today: research-map-plus-grid's
|
||||
// gridLanguage() falls back to `linked.language.value || linked.language.label`, and `label` is a
|
||||
// DISPLAY NAME. The grid's valueMatches() compares whole normalised values, so a name fetched in
|
||||
// one language matches no row in another — the failure that took the grid 2,107 -> 0 and read as
|
||||
// "no results" (rega, 2026-09-26). This asserts the CLASS, not today's instance.
|
||||
{
|
||||
const locale = ref('en')
|
||||
// A language whose display label follows the locale, as the API returns it, but whose code does not.
|
||||
const NAMES = { en: 'Arabic, Sudanese', ar: 'العربية السودانية' }
|
||||
const linkedLanguage = () => ({ codes: ['apd'], value: 'apd', label: NAMES[locale.value] })
|
||||
const gridLanguage = () => {
|
||||
const l = linkedLanguage()
|
||||
const codes = Array.isArray(l.codes) ? l.codes : []
|
||||
if (codes.length > 1) return codes.join('|')
|
||||
return l.value || l.label
|
||||
}
|
||||
const grid = makeEl('grid')
|
||||
const paneConfig = usePaneConfig(() => makeHost([grid]))
|
||||
paneConfig.registerAttrs(grid, () => ({ 'primary-language': gridLanguage(), 'wagf-region': `r-${locale.value === 'x' ? 1 : 0}` }))
|
||||
await nextTick()
|
||||
const beforeSwitch = grid.getAttribute('primary-language')
|
||||
locale.value = 'ar'
|
||||
await nextTick()
|
||||
ok(grid.getAttribute('primary-language') === beforeSwitch,
|
||||
`a locale change leaves primary-language unchanged ('${beforeSwitch}' -> '${grid.getAttribute('primary-language')}')`)
|
||||
ok(!/[-ۿ]/.test(grid.getAttribute('primary-language')),
|
||||
'the filter is a CODE, never a localised display name')
|
||||
paneConfig.dispose()
|
||||
}
|
||||
|
||||
// ── 8. DISPOSE STOPS THE EFFECT. A composite unmounted and left watching is a leak that keeps
|
||||
// writing to detached elements.
|
||||
{
|
||||
const outer = ref({ locale: 'en' })
|
||||
const el = makeEl()
|
||||
const paneConfig = usePaneConfig(() => makeHost([el]))
|
||||
paneConfig.register(el, () => ({ ...outer.value }))
|
||||
await nextTick()
|
||||
paneConfig.dispose()
|
||||
const after = el.writes
|
||||
outer.value = { locale: 'ar' }
|
||||
await nextTick()
|
||||
ok(el.writes === after, 'after dispose() the outer config is no longer followed')
|
||||
ok(paneConfig.count() === 0, 'and the entry list is emptied')
|
||||
}
|
||||
|
||||
// ── 9. IT DOES NOT THROW OUTSIDE setup(). Every case above runs outside a component instance, which
|
||||
// is the un-guarded onBeforeUnmount hazard studio raised — if that guard regressed, this file would
|
||||
// have failed at the first register() rather than here. Stated so the reason is on the record.
|
||||
ok(true, 'the whole file ran outside setup(): the getCurrentInstance() guard holds')
|
||||
|
||||
if (bad) {
|
||||
console.error(`\nPANE-CONFIG-REFORWARD: FAIL — ${bad} case(s). A composite's panes no longer follow the`)
|
||||
console.error('outer profile-config, so a live locale switch leaves every nested pane in the old language.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('\nPANE-CONFIG-REFORWARD: PASS — panes follow the outer config for the session, and a locale change never rewrites a filter value.')
|
||||
368
library/components/info/MapInfo.vue
Normal file
368
library/components/info/MapInfo.vue
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
<script setup>
|
||||
/**
|
||||
* MapInfo.vue — a map's "what can this do for you?" button, one-time pointer and example popup.
|
||||
*
|
||||
* The popup answers the question with REAL output the map makes.
|
||||
*
|
||||
* WHY: a map shown to someone with nobody beside them to explain it has to explain itself. The
|
||||
* popup does that by showing what you walk away with (a poster, a prayer packet, a deck of cards, a
|
||||
* spreadsheet), pre-generated, so it opens instantly instead of rendering for seconds.
|
||||
*
|
||||
* ★ EXAMPLES ARE A LIST, NOT A FIXED POSTER/PACKET/CARDS TRIPLE. Maps make different things: the
|
||||
* UPG-100 and Regions maps make posters, packets and cards; Big Picture makes only a spreadsheet
|
||||
* (its export is a CSV and it has no poster or packet anywhere). A fixed triple would promise a
|
||||
* button a map does not have. Each item is one artefact, in the order given:
|
||||
* { kind: 'image', src, full?, alt?, titleKey? } a poster or any single picture; `full` opens
|
||||
* the full-size file in a new tab
|
||||
* { kind: 'pages', pages: [src, …], titleKey? } the first page; expands to every page and back
|
||||
* { kind: 'deck', cards: [src, …], titleKey? } a stacked deck; expands to every card and back
|
||||
* { kind: 'table', columns: [..], rows: [[..]..], total?, titleKey? }
|
||||
* a small real excerpt of a spreadsheet; `total`
|
||||
* says how many rows the full export has. A
|
||||
* column is a ready string or { key } — pass the
|
||||
* table's own header keys so an Arabic reader does
|
||||
* not get English headers. Number cells are
|
||||
* formatted in the reader's locale.
|
||||
* Relative srcs resolve against `assetsBase`, so the same list works in any tree.
|
||||
*
|
||||
* ★ THE POINTER GOES FOR GOOD, per map, once its popup has been opened AND closed (opening alone
|
||||
* could be a mis-tap). The checkbox in the popup turns the pointer off on EVERY map. Both live in
|
||||
* localStorage, read through a guard: private browsing and sandboxed frames throw on access, and a
|
||||
* hint must never be the thing that breaks a page. The button itself always stays.
|
||||
*
|
||||
* ★ TEXT. The component's own words are in the 'mapInfo' catalogue area, in all eleven locales, and
|
||||
* so are the per-map words, as mapInfo.tab.<tabId>.lead / .takeaway / .note, passed in as KEYS (ready
|
||||
* strings also work). NOT in each map's own area: a bundle ships only the catalogue areas its source
|
||||
* names (internal/i18n-areas.mjs), and the gallery that mounts one of these per map names none of the
|
||||
* maps' areas, so a key there would render blank; naming them would pull every map's whole catalogue
|
||||
* into the gallery. Everything is resolved at render, never stored, so a live language switch
|
||||
* re-translates it. A host without vue-i18n has only the page-global locale, which
|
||||
* nothing observes; pass `locale` from a host that tracks it, and the popup also re-reads the
|
||||
* global every time it opens.
|
||||
*
|
||||
* Usage (one per map, inside a positioned container):
|
||||
* <MapInfo storage-key="upg-100" lead-key="…" :examples="[…]" :assets-base="assetsBase" />
|
||||
*/
|
||||
import { ref, reactive, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { useOptionalT } from '@map/composables/useOptionalT.js'
|
||||
import { tStatic, getRuntimeLocale } from '@map/i18n/index.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** One per map. Keys the "this map's pointer has been seen" flag. */
|
||||
storageKey: { type: String, required: true },
|
||||
/** The map's own sentence: what it does for the reader. A catalogue key, or a ready string. */
|
||||
leadKey: { type: String, default: '' },
|
||||
lead: { type: String, default: '' },
|
||||
/** What the reader takes away. Key or string, optional. */
|
||||
takeawayKey: { type: String, default: '' },
|
||||
takeaway: { type: String, default: '' },
|
||||
/** A caveat under the examples (e.g. which countries are not supported yet). Key or string. */
|
||||
noteKey: { type: String, default: '' },
|
||||
note: { type: String, default: '' },
|
||||
/** The artefacts, in display order. See the header for the four kinds. */
|
||||
examples: { type: Array, default: () => [] },
|
||||
/** Base for relative example srcs, normally the tree's assets folder. */
|
||||
assetsBase: { type: String, default: '' },
|
||||
/** Where the button sits in its container. */
|
||||
placement: { type: String, default: 'top-right', validator: (v) => ['top-right', 'top-left'].includes(v) },
|
||||
/** The host's current locale, when it tracks one. */
|
||||
locale: { type: String, default: '' },
|
||||
})
|
||||
|
||||
// ── locale and text ──────────────────────────────────────────────────────────────────────────────
|
||||
// `opens` is read by every lookup, so opening the popup re-reads the page-global locale on hosts
|
||||
// with no reactive one.
|
||||
const opens = ref(0)
|
||||
function lang() {
|
||||
void opens.value
|
||||
return String(props.locale || getRuntimeLocale() || 'en').split('-')[0].toLowerCase()
|
||||
}
|
||||
const t = useOptionalT('', { fallback: (q) => tStatic(q, lang()) })
|
||||
const text = (key, str) => (key ? t(key) : str)
|
||||
const num = (n) => { try { return new Intl.NumberFormat(lang()).format(n) } catch { return String(n) } }
|
||||
const isRtl = () => lang() === 'ar'
|
||||
|
||||
// ── storage, guarded ─────────────────────────────────────────────────────────────────────────────
|
||||
const SEEN = 'dxmaps.mapInfo.seen.'
|
||||
const OFF = 'dxmaps.mapInfo.pointer'
|
||||
const OFF_EVENT = 'dxmaps:mapinfo-pointer-off'
|
||||
function readStore(k) { try { return window.localStorage.getItem(k) } catch { return null } }
|
||||
function writeStore(k, v) { try { window.localStorage.setItem(k, v) } catch { /* private mode: in-memory only */ } }
|
||||
|
||||
const pointing = ref(false)
|
||||
const pointerOff = ref(false)
|
||||
function refreshPointer() {
|
||||
pointerOff.value = readStore(OFF) === 'off'
|
||||
pointing.value = !pointerOff.value && readStore(SEEN + props.storageKey) !== '1'
|
||||
}
|
||||
// Another map on the page (same document) or another tab (storage event) turned the pointer off.
|
||||
function onPointerOff() { refreshPointer() }
|
||||
function onStorage(e) { if (!e || e.key === OFF || e.key === SEEN + props.storageKey) refreshPointer() }
|
||||
onMounted(() => {
|
||||
refreshPointer()
|
||||
window.addEventListener(OFF_EVENT, onPointerOff)
|
||||
window.addEventListener('storage', onStorage)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener(OFF_EVENT, onPointerOff)
|
||||
window.removeEventListener('storage', onStorage)
|
||||
})
|
||||
function setPointerOff(off) {
|
||||
pointerOff.value = off
|
||||
writeStore(OFF, off ? 'off' : 'on')
|
||||
pointing.value = !off && readStore(SEEN + props.storageKey) !== '1'
|
||||
try { window.dispatchEvent(new CustomEvent(OFF_EVENT)) } catch { /* no window */ }
|
||||
}
|
||||
|
||||
// ── open / close ─────────────────────────────────────────────────────────────────────────────────
|
||||
const open = ref(false)
|
||||
const btnEl = ref(null)
|
||||
const closeEl = ref(null)
|
||||
const expanded = reactive({})
|
||||
const failed = reactive({})
|
||||
async function show() {
|
||||
opens.value++
|
||||
open.value = true
|
||||
await nextTick()
|
||||
closeEl.value?.focus()
|
||||
}
|
||||
async function hide() {
|
||||
open.value = false
|
||||
writeStore(SEEN + props.storageKey, '1')
|
||||
pointing.value = false
|
||||
await nextTick()
|
||||
btnEl.value?.focus()
|
||||
}
|
||||
// ★ aria-modal PROMISES THE READER THAT TAB STAYS INSIDE, so keep it there: Tab from the last control
|
||||
// wraps to the first, Shift+Tab from the first to the last. The active element is read from the shadow
|
||||
// root (document.activeElement would be the host element). Hidden controls are skipped.
|
||||
const dialogEl = ref(null)
|
||||
function trapTab(e) {
|
||||
const box = dialogEl.value
|
||||
if (!box) return
|
||||
const items = [...box.querySelectorAll('button, a[href], input, [tabindex]:not([tabindex="-1"])')]
|
||||
.filter((el) => !el.disabled && el.offsetParent !== null)
|
||||
if (!items.length) return
|
||||
const active = box.getRootNode().activeElement
|
||||
const first = items[0], last = items[items.length - 1]
|
||||
if (e.shiftKey && (active === first || !box.contains(active))) { e.preventDefault(); last.focus() }
|
||||
else if (!e.shiftKey && (active === last || !box.contains(active))) { e.preventDefault(); first.focus() }
|
||||
}
|
||||
function onKey(e) {
|
||||
if (e.key === 'Escape') { e.stopPropagation(); hide() }
|
||||
else if (e.key === 'Tab') trapTab(e)
|
||||
}
|
||||
function toggle(i) { expanded[i] = !expanded[i] }
|
||||
// ★ A MISSING EXAMPLE IMAGE DISAPPEARS, QUIETLY. The examples are pre-generated files that ship in the
|
||||
// tree; if one is absent (a shoot not yet landed, a tree copied without it) the reader must see neither
|
||||
// a broken-image icon nor an empty frame. An artefact whose lead image fails is dropped whole, title
|
||||
// and all (failed[i]); a single page or card that fails later just hides itself. The text above the
|
||||
// examples still reads on its own.
|
||||
function dropImg(e) { const el = e && e.target; if (el) el.style.display = 'none' }
|
||||
|
||||
// ── examples ─────────────────────────────────────────────────────────────────────────────────────
|
||||
function url(src) {
|
||||
const s = String(src || '')
|
||||
if (!s || /^(?:[a-z]+:|\/)/i.test(s) || !props.assetsBase) return s
|
||||
return props.assetsBase.replace(/\/+$/, '') + '/' + s.replace(/^\.?\//, '')
|
||||
}
|
||||
const DEFAULT_TITLE = { image: 'mapInfo.poster', pages: 'mapInfo.packet', deck: 'mapInfo.cards', table: 'mapInfo.spreadsheet' }
|
||||
function titleOf(ex) { return t(ex.titleKey || DEFAULT_TITLE[ex.kind] || 'mapInfo.example') }
|
||||
const deckTop = (ex) => (ex.cards || []).slice(0, 5)
|
||||
const colLabel = (c) => (c && typeof c === 'object' ? (c.key ? t(c.key) : String(c.label || '')) : String(c ?? ''))
|
||||
|
||||
useShadowStyles(`
|
||||
.mi-btn{ position:absolute; z-index:20; top:var(--map-info-top, 12px); width:40px; height:40px;
|
||||
border-radius:50%; border:1px solid rgba(0,0,0,.18); background:#fff; color:#1f2a44; cursor:pointer;
|
||||
display:inline-flex; align-items:center; justify-content:center; padding:0;
|
||||
box-shadow:0 2px 8px rgba(0,0,0,.18); font:inherit; }
|
||||
.mi-btn:hover{ background:#f3f5fa; }
|
||||
.mi-btn:focus-visible{ outline:3px solid #2563eb; outline-offset:2px; }
|
||||
.mi-btn svg{ width:22px; height:22px; }
|
||||
.mi-at-right{ right:var(--map-info-side, 12px); }
|
||||
.mi-at-left{ left:var(--map-info-side, 12px); }
|
||||
.mi-pointer{ position:absolute; z-index:20; top:calc(var(--map-info-top, 12px) + 4px);
|
||||
display:flex; align-items:center; gap:6px; pointer-events:none; white-space:nowrap;
|
||||
background:#1f2a44; color:#fff; font-size:13px; font-weight:600; line-height:1; padding:8px 12px;
|
||||
border-radius:999px; box-shadow:0 2px 10px rgba(0,0,0,.25); animation:mi-nudge 1.4s ease-in-out infinite; }
|
||||
.mi-pointer.mi-at-right{ right:calc(var(--map-info-side, 12px) + 50px); }
|
||||
.mi-pointer.mi-at-left{ left:calc(var(--map-info-side, 12px) + 50px); flex-direction:row-reverse;
|
||||
animation-name:mi-nudge-left; }
|
||||
.mi-pointer svg{ width:16px; height:16px; flex:none; }
|
||||
@keyframes mi-nudge{ 0%,100%{ transform:translateX(0) } 50%{ transform:translateX(6px) } }
|
||||
@keyframes mi-nudge-left{ 0%,100%{ transform:translateX(0) } 50%{ transform:translateX(-6px) } }
|
||||
|
||||
.mi-scrim{ position:fixed; inset:0; z-index:1000; background:rgba(15,20,35,.55);
|
||||
display:flex; align-items:center; justify-content:center; padding:24px; }
|
||||
.mi-dialog{ background:#fff; color:#1b1f2a; width:min(720px, 100%); max-height:min(88vh, 100%);
|
||||
border-radius:14px; box-shadow:0 20px 60px rgba(0,0,0,.35); display:flex; flex-direction:column;
|
||||
overflow:hidden; font-size:15px; line-height:1.5; }
|
||||
.mi-head{ display:flex; align-items:flex-start; gap:12px; padding:18px 20px 12px; border-bottom:1px solid #e6e8ee; }
|
||||
.mi-title{ margin:0; font-size:19px; line-height:1.3; flex:1; }
|
||||
.mi-close{ flex:none; width:40px; height:40px; border-radius:50%; border:none; background:#eef1f6;
|
||||
color:#1b1f2a; cursor:pointer; font-size:22px; line-height:1; }
|
||||
.mi-close:focus-visible, .mi-toggle:focus-visible, .mi-hit:focus-visible, .mi-full:focus-visible{
|
||||
outline:3px solid #2563eb; outline-offset:2px; }
|
||||
.mi-body{ overflow-y:auto; padding:16px 20px 20px; -webkit-overflow-scrolling:touch; }
|
||||
.mi-lead{ margin:0 0 6px; font-size:16px; }
|
||||
.mi-takeaway{ margin:0 0 14px; color:#3b4256; }
|
||||
.mi-takeaway b{ color:#1b1f2a; }
|
||||
.mi-ex{ margin:18px 0 0; }
|
||||
.mi-ex h3{ margin:0 0 8px; font-size:15px; text-transform:uppercase; letter-spacing:.04em; color:#556; }
|
||||
.mi-img{ display:block; width:100%; height:auto; border-radius:8px; background:#f4f5f8;
|
||||
box-shadow:0 1px 4px rgba(0,0,0,.15); }
|
||||
.mi-poster{ max-width:480px; margin:0 auto; }
|
||||
.mi-full{ display:inline-block; margin-top:6px; font-size:14px; color:#1d4ed8; }
|
||||
.mi-hit{ display:block; width:100%; padding:0; border:none; background:none; cursor:zoom-in; text-align:inherit; }
|
||||
.mi-hit.mi-open{ cursor:zoom-out; }
|
||||
.mi-page{ max-width:420px; margin:0 auto; }
|
||||
.mi-pages .mi-page + .mi-page{ margin-top:12px; }
|
||||
.mi-stack{ position:relative; }
|
||||
.mi-stack::after{ content:''; position:absolute; inset:6px -6px -6px 6px; z-index:-1; border-radius:8px;
|
||||
background:#e3e6ee; box-shadow:0 1px 4px rgba(0,0,0,.15); }
|
||||
.mi-toggle{ display:inline-flex; align-items:center; min-height:44px; margin-top:10px; padding:0 16px;
|
||||
border-radius:999px; border:1px solid #c9cedb; background:#fff; color:#1b1f2a; font:inherit;
|
||||
font-size:14px; font-weight:600; cursor:pointer; }
|
||||
.mi-toggle:hover{ background:#f3f5fa; }
|
||||
.mi-center{ text-align:center; }
|
||||
.mi-deck{ position:relative; height:250px; max-width:340px; margin:0 auto; }
|
||||
.mi-deck .mi-card{ position:absolute; top:10px; width:160px; height:224px; object-fit:cover;
|
||||
border-radius:8px; box-shadow:0 2px 8px rgba(0,0,0,.25); background:#f4f5f8; }
|
||||
.mi-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(120px, 1fr)); gap:10px; }
|
||||
.mi-grid .mi-img{ aspect-ratio:5 / 7; object-fit:cover; }
|
||||
.mi-table-wrap{ overflow-x:auto; border:1px solid #e1e4ec; border-radius:8px; }
|
||||
.mi-table{ border-collapse:collapse; width:100%; font-size:13px; }
|
||||
.mi-table th, .mi-table td{ padding:6px 10px; border-bottom:1px solid #eceef3; text-align:start; white-space:nowrap; }
|
||||
.mi-table th{ background:#f5f6f9; font-weight:600; }
|
||||
.mi-more{ margin:6px 0 0; font-size:13px; color:#556; }
|
||||
.mi-note{ margin:18px 0 0; padding:10px 12px; border-radius:8px; background:#fff7e6; color:#5a4300; font-size:14px; }
|
||||
.mi-foot{ padding:12px 20px; border-top:1px solid #e6e8ee; font-size:14px; }
|
||||
.mi-foot label{ display:inline-flex; align-items:center; gap:8px; min-height:44px; cursor:pointer; }
|
||||
.mi-foot input{ width:20px; height:20px; }
|
||||
|
||||
/* A phone gets the whole screen: a floating card with margins leaves too little room for a page. */
|
||||
@media (max-width:600px){
|
||||
.mi-scrim{ padding:0; align-items:stretch; }
|
||||
.mi-dialog{ width:100%; max-height:none; height:100%; border-radius:0; }
|
||||
.mi-pointer{ font-size:12px; padding:7px 10px; }
|
||||
.mi-deck{ height:220px; }
|
||||
.mi-deck .mi-card{ width:136px; height:190px; }
|
||||
}
|
||||
/* This component animates (the pointer's nudge), so it carries its own reduced-motion rule where its
|
||||
styles are injected: a page-level rule cannot reach into a shadow root. */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
.mi-pointer{ animation:none; }
|
||||
}
|
||||
`, 'map-info')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mi" :dir="isRtl() ? 'rtl' : 'ltr'" :lang="lang()">
|
||||
<button ref="btnEl" type="button" class="mi-btn" :class="placement === 'top-left' ? 'mi-at-left' : 'mi-at-right'"
|
||||
:aria-label="t('mapInfo.open')" :title="t('mapInfo.open')" aria-haspopup="dialog" :aria-expanded="String(open)"
|
||||
@click="show">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<rect x="11" y="10" width="2" height="7" rx="1" fill="currentColor" />
|
||||
<circle cx="12" cy="7" r="1.3" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div v-if="pointing && !open" class="mi-pointer" :class="placement === 'top-left' ? 'mi-at-left' : 'mi-at-right'" aria-hidden="true">
|
||||
<span>{{ t('mapInfo.pointer') }}</span>
|
||||
<svg viewBox="0 0 16 16" focusable="false">
|
||||
<path v-if="placement === 'top-left'" d="M14 8H3M7 3.5 2.5 8 7 12.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path v-else d="M2 8h11M9 3.5 13.5 8 9 12.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div v-if="open" class="mi-scrim" @click.self="hide" @keydown="onKey">
|
||||
<div ref="dialogEl" class="mi-dialog" role="dialog" aria-modal="true" :aria-labelledby="'mi-title-' + storageKey">
|
||||
<div class="mi-head">
|
||||
<h2 :id="'mi-title-' + storageKey" class="mi-title">{{ t('mapInfo.title') }}</h2>
|
||||
<button ref="closeEl" type="button" class="mi-close" :aria-label="t('mapInfo.close')" @click="hide">×</button>
|
||||
</div>
|
||||
|
||||
<div class="mi-body">
|
||||
<p v-if="leadKey || lead" class="mi-lead">{{ text(leadKey, lead) }}</p>
|
||||
<p v-if="takeawayKey || takeaway" class="mi-takeaway"><b>{{ t('mapInfo.takeaway') }}</b> {{ text(takeawayKey, takeaway) }}</p>
|
||||
|
||||
<section v-for="(ex, i) in examples" v-show="!failed[i]" :key="i" class="mi-ex">
|
||||
<h3>{{ titleOf(ex) }}</h3>
|
||||
|
||||
<template v-if="ex.kind === 'image'">
|
||||
<img class="mi-img mi-poster" :src="url(ex.src)" :alt="ex.alt || titleOf(ex)" decoding="async" @error="failed[i] = true" />
|
||||
<div v-if="ex.full" class="mi-center">
|
||||
<a class="mi-full" :href="url(ex.full)" target="_blank" rel="noopener">{{ t('mapInfo.openFull') }}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="ex.kind === 'pages' && (ex.pages || []).length">
|
||||
<template v-if="!expanded[i]">
|
||||
<button type="button" class="mi-hit mi-page" :class="{ 'mi-stack': ex.pages.length > 1 }"
|
||||
:aria-label="t('mapInfo.expandPages', { n: num(ex.pages.length) })" aria-expanded="false" @click="toggle(i)">
|
||||
<img class="mi-img" :src="url(ex.pages[0])" :alt="titleOf(ex)" decoding="async" @error="failed[i] = true" />
|
||||
</button>
|
||||
<div v-if="ex.pages.length > 1" class="mi-center">
|
||||
<button type="button" class="mi-toggle" aria-expanded="false" @click="toggle(i)">{{ t('mapInfo.expandPages', { n: num(ex.pages.length) }) }}</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="mi-center"><button type="button" class="mi-toggle" aria-expanded="true" @click="toggle(i)">{{ t('mapInfo.collapsePages') }}</button></div>
|
||||
<div class="mi-pages">
|
||||
<button v-for="(p, k) in ex.pages" :key="k" type="button" class="mi-hit mi-open mi-page"
|
||||
:aria-label="t('mapInfo.collapsePages')" @click="toggle(i)">
|
||||
<img class="mi-img" :src="url(p)" :alt="titleOf(ex) + ' ' + num(k + 1)" loading="lazy" decoding="async" @error="dropImg" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="mi-center"><button type="button" class="mi-toggle" aria-expanded="true" @click="toggle(i)">{{ t('mapInfo.collapsePages') }}</button></div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="ex.kind === 'deck' && (ex.cards || []).length">
|
||||
<template v-if="!expanded[i]">
|
||||
<button type="button" class="mi-hit mi-deck" :aria-label="t('mapInfo.expandDeck', { n: num(ex.cards.length) })"
|
||||
aria-expanded="false" @click="toggle(i)">
|
||||
<img v-for="(c, k) in deckTop(ex)" :key="k" class="mi-card" :src="url(c)" alt="" decoding="async" @error="k === 0 ? (failed[i] = true) : dropImg($event)"
|
||||
:style="{ insetInlineStart: (k * 36) + 'px', transform: 'rotate(' + ((k - 2) * 4) + 'deg)', zIndex: 10 - k }" />
|
||||
</button>
|
||||
<div class="mi-center">
|
||||
<button type="button" class="mi-toggle" aria-expanded="false" @click="toggle(i)">{{ t('mapInfo.expandDeck', { n: num(ex.cards.length) }) }}</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="mi-center"><button type="button" class="mi-toggle" aria-expanded="true" @click="toggle(i)">{{ t('mapInfo.collapseDeck') }}</button></div>
|
||||
<div class="mi-grid">
|
||||
<img v-for="(c, k) in ex.cards" :key="k" class="mi-img" :src="url(c)" :alt="titleOf(ex) + ' ' + num(k + 1)" loading="lazy" decoding="async" @error="dropImg" />
|
||||
</div>
|
||||
<div class="mi-center"><button type="button" class="mi-toggle" aria-expanded="true" @click="toggle(i)">{{ t('mapInfo.collapseDeck') }}</button></div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="ex.kind === 'table' && (ex.columns || []).length">
|
||||
<div class="mi-table-wrap">
|
||||
<table class="mi-table">
|
||||
<thead><tr><th v-for="(c, k) in ex.columns" :key="k" scope="col">{{ colLabel(c) }}</th></tr></thead>
|
||||
<tbody><tr v-for="(r, k) in ex.rows || []" :key="k"><td v-for="(v, j) in r" :key="j">{{ typeof v === 'number' ? num(v) : v }}</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-if="ex.total && ex.total > (ex.rows || []).length" class="mi-more">
|
||||
{{ t('mapInfo.moreRows', { n: num(ex.total - (ex.rows || []).length) }) }}
|
||||
</p>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<p v-if="noteKey || note" class="mi-note">{{ text(noteKey, note) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mi-foot">
|
||||
<label>
|
||||
<input type="checkbox" :checked="pointerOff" @change="setPointerOff($event.target.checked)" />
|
||||
<span>{{ t('mapInfo.pointerOff') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
169
library/composables/usePaneConfig.js
Normal file
169
library/composables/usePaneConfig.js
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/**
|
||||
* usePaneConfig — keep a composite's panes' profile-config in step with the outer element's.
|
||||
*
|
||||
* ★ THE BUG THIS EXISTS FOR. A composite builds each pane by creating the child custom element and
|
||||
* writing its profile-config ONCE, inside onMounted:
|
||||
*
|
||||
* el.setAttribute('profile-config', JSON.stringify(pcfg)) // and never again
|
||||
*
|
||||
* That is correct at mount and wrong for the rest of the session. The pane is handed a JSON STRING,
|
||||
* not a reactive object, so a later change to the OUTER element's profile-config updates the
|
||||
* composite's own `cfg` and reaches nothing below it. MEASURED on Regions: a live locale switch
|
||||
* en -> ar left the panes in English for the whole session, because regions-dashboard is kept alive
|
||||
* by v-show rather than remounted — the one code path that would have rewritten the attribute never
|
||||
* runs again. The leaf's own chain is fully reactive and was never at fault: it is reactive to its
|
||||
* own prop, and its prop is frozen.
|
||||
*
|
||||
* ★ WHY A HELPER RATHER THAN THREE FIXES. regions-mfe, upg-100-mfe and country-posters-mfe all build
|
||||
* their panes the same way — assemble a config object, stringify it, set the attribute once. They
|
||||
* differ only in WHICH keys they inherit. So the composite keeps its own INHERITED_PAGE_KEYS and its
|
||||
* own assembly, and hands the result here; this owns the part that was missing everywhere.
|
||||
*
|
||||
* ★ IT WRITES ONLY WHEN THE STRING CHANGES. Setting profile-config re-parses it in the child and can
|
||||
* remount what it drives, so an unconditional write on every reactive tick would be a rerender loop.
|
||||
* The previous value is remembered per element and compared before touching the DOM.
|
||||
*
|
||||
* const panes = usePaneConfig(() => stage.value, TAG)
|
||||
* panes.register(el, () => paneConfigFor(pane)) // at mount, after appendChild
|
||||
*
|
||||
* Re-forwarding happens inside a watchEffect, so any reactive source the getter reads — the outer
|
||||
* config, a computed spec, the pane's own config — triggers it.
|
||||
*
|
||||
* ★ NOT EVERY PANE IS CONFIGURED BY profile-config. people-groups-grid takes its filters as THREE
|
||||
* separate attributes (wagf-region, religion-family, primary-language) rather than one JSON blob, so
|
||||
* research-map-plus-grid has one pane of each kind. registerAttrs covers that second shape:
|
||||
*
|
||||
* panes.registerAttrs(grid, () => ({ 'wagf-region': region.value, 'religion-family': fam.value }))
|
||||
*
|
||||
* It shares this list, this watchEffect, this prune and this write-only-on-change — deliberately, so
|
||||
* the second adopter rides ONE mechanism instead of forking a second. A helper with a single caller
|
||||
* has not been shown to generalise at all; this is the shape that showed it.
|
||||
*/
|
||||
import { watchEffect, onBeforeUnmount, shallowReactive, getCurrentInstance } from 'vue'
|
||||
|
||||
/**
|
||||
* @param {() => (Element|null|undefined)} hostGetter the element the panes were appended under; used
|
||||
* only to drop registrations whose element has left the DOM (a pane column can be rebuilt).
|
||||
* ★ IT MUST BE IN THE SAME TREE AS THE PANES. Element.contains() does NOT cross a shadow
|
||||
* boundary, so passing the custom element while its panes live in its shadow root would make
|
||||
* every pane look detached and skip every write, silently.
|
||||
* @returns {{ register: (el: Element, configGetter: () => object) => void,
|
||||
* registerAttrs: (el: Element, attrsGetter: () => Record<string,unknown>) => void,
|
||||
* dispose: () => void, count: () => number }}
|
||||
*/
|
||||
export function usePaneConfig(hostGetter) {
|
||||
// ★ shallowReactive, NOT a plain array. The watchEffect below runs once, at the FIRST register();
|
||||
// with a plain array a LATER entry's getter is never tracked, so its pane stays frozen until
|
||||
// something the first entry reads happens to change. MEASURED with real Vue by studio: two panes
|
||||
// reading different refs, changing only pane 2's source produced ZERO writes. That is the very bug
|
||||
// this helper exists to fix, reappearing for any adopter whose panes read different sources.
|
||||
// Making the list reactive re-runs the effect on register() and re-tracks every getter.
|
||||
/**
|
||||
* One entry per registered pane. `attrs` is the ATTRIBUTE NAMES this entry owns: the single-key
|
||||
* ['profile-config'] for a JSON-configured pane, or the caller's list for an attribute-configured
|
||||
* one. `last` holds the previously written value PER ATTRIBUTE, so a getter that changes one of
|
||||
* three attributes writes one of three.
|
||||
* @type {Array<{el: Element, get: () => object, attrs: string[]|null, last: Record<string,string|null>, warned: boolean}>}
|
||||
*/
|
||||
const entries = shallowReactive([])
|
||||
let stop = null
|
||||
|
||||
const alive = (el) => {
|
||||
if (!el || !el.isConnected) return false
|
||||
const host = typeof hostGetter === 'function' ? hostGetter() : hostGetter
|
||||
// A pane column rebuild replaces the elements; an entry whose element is no longer under the
|
||||
// current host is stale and must not be written to.
|
||||
return !host || host.contains(el)
|
||||
}
|
||||
|
||||
/** Add an entry and make sure the one shared effect is running. */
|
||||
function add(entry) {
|
||||
entries.push(entry)
|
||||
if (stop) return
|
||||
stop = watchEffect(() => {
|
||||
// PRUNE rather than skip: an adopter that rebuilds its pane column would otherwise keep
|
||||
// detached elements in the list and re-run their getters on every pass, forever.
|
||||
for (let i = entries.length - 1; i >= 0; i--) if (!alive(entries[i].el)) entries.splice(i, 1)
|
||||
for (const e of entries) forward(e)
|
||||
})
|
||||
}
|
||||
|
||||
/** Re-evaluate one entry's getter and write only what changed. */
|
||||
function forward(entry) {
|
||||
let value
|
||||
// A getter that throws must not take the other panes down with it: this runs inside a
|
||||
// watchEffect, where an exception would stop every later entry in the same pass. It is
|
||||
// reported ONCE per entry -- silence here would recreate the symptom this helper fixes.
|
||||
try { value = entry.get() } catch (e) {
|
||||
if (!entry.warned) { entry.warned = true; console.error('[usePaneConfig] pane config getter threw; this pane will not follow the outer config:', e) }
|
||||
return
|
||||
}
|
||||
if (!entry.attrs) {
|
||||
// JSON pane: the whole config is one attribute.
|
||||
let next
|
||||
try { next = JSON.stringify(value) } catch (e) {
|
||||
if (!entry.warned) { entry.warned = true; console.error('[usePaneConfig] pane config did not stringify; this pane will not follow the outer config:', e) }
|
||||
return
|
||||
}
|
||||
if (next === entry.last['profile-config']) return
|
||||
entry.last['profile-config'] = next
|
||||
entry.el.setAttribute('profile-config', next)
|
||||
return
|
||||
}
|
||||
// Attribute pane: each named attribute is compared and written independently, so a getter whose
|
||||
// region changed does not rewrite (and so re-trigger) the two that did not.
|
||||
const bag = value && typeof value === 'object' ? value : {}
|
||||
for (const name of entry.attrs) {
|
||||
// An absent/null/undefined value is written as '' rather than the string "undefined" -- the
|
||||
// grid reads these as plain filter strings and "undefined" would be a filter that matches
|
||||
// nothing, which is the empty-grid failure this repo has already paid for once.
|
||||
const raw = bag[name]
|
||||
const next = raw === undefined || raw === null ? '' : String(raw)
|
||||
if (next === entry.last[name]) continue
|
||||
entry.last[name] = next
|
||||
entry.el.setAttribute(name, next)
|
||||
}
|
||||
}
|
||||
|
||||
function register(el, configGetter) {
|
||||
if (!el || typeof configGetter !== 'function') return
|
||||
// The attribute the caller already set at mount is the baseline: recording it here means the
|
||||
// first watchEffect pass does not rewrite an identical value.
|
||||
add({ el, get: configGetter, attrs: null, last: { 'profile-config': el.getAttribute('profile-config') }, warned: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* The attribute-configured sibling. `attrsGetter` returns an object keyed by ATTRIBUTE NAME
|
||||
* (kebab-case, as written in the DOM), e.g. { 'wagf-region': 'africa' }.
|
||||
* ★ THE NAME LIST IS FIXED AT REGISTRATION, from the getter's first result. A pane's attribute
|
||||
* surface is static -- it is the element's contract -- and pinning it means a later getter that
|
||||
* omits a key CLEARS that attribute rather than silently leaving a stale filter applied, which is
|
||||
* the same class of lie as the frozen config this helper exists to fix.
|
||||
*/
|
||||
function registerAttrs(el, attrsGetter) {
|
||||
if (!el || typeof attrsGetter !== 'function') return
|
||||
let first
|
||||
try { first = attrsGetter() } catch (e) {
|
||||
console.error('[usePaneConfig] attrs getter threw at registration; this pane is not registered:', e)
|
||||
return
|
||||
}
|
||||
const attrs = Object.keys(first && typeof first === 'object' ? first : {})
|
||||
if (!attrs.length) {
|
||||
console.error('[usePaneConfig] attrs getter returned no attribute names; this pane is not registered.')
|
||||
return
|
||||
}
|
||||
const last = {}
|
||||
for (const name of attrs) last[name] = el.getAttribute(name)
|
||||
add({ el, get: attrsGetter, attrs, last, warned: false })
|
||||
}
|
||||
|
||||
// ★ GUARDED, the house pattern (useMapData.js:144). onBeforeUnmount only binds inside setup(); a
|
||||
// caller outside one gets a Vue warning and no auto-cleanup. `dispose` is returned so such a
|
||||
// caller has something to call -- silently leaking a watchEffect would be the worse half.
|
||||
const dispose = () => { if (stop) { stop(); stop = null } entries.length = 0 }
|
||||
if (getCurrentInstance()) onBeforeUnmount(dispose)
|
||||
|
||||
return { register, registerAttrs, dispose, count: () => entries.length }
|
||||
}
|
||||
|
||||
export default usePaneConfig
|
||||
50
library/i18n/locales/ar/mapInfo.json
Normal file
50
library/i18n/locales/ar/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "ماذا يمكن أن تقدّم لك هذه الخريطة؟",
|
||||
"title": "ماذا يمكن أن تقدّم لك هذه الخريطة؟",
|
||||
"pointer": "ماذا تفعل هذه الخريطة؟",
|
||||
"close": "إغلاق",
|
||||
"takeaway": "ما تحصل عليه:",
|
||||
"poster": "ملصق",
|
||||
"packet": "حزمة الصلاة",
|
||||
"cards": "بطاقات الصلاة",
|
||||
"spreadsheet": "جدول بيانات",
|
||||
"example": "مثال",
|
||||
"expandPages": "عرض كل الصفحات ({n})",
|
||||
"collapsePages": "عرض الصفحة الأولى فقط",
|
||||
"expandDeck": "عرض كل البطاقات ({n})",
|
||||
"collapseDeck": "تجميع البطاقات",
|
||||
"moreRows": "صفوف إضافية في جدول البيانات الكامل: {n}",
|
||||
"openFull": "فتح بالحجم الكامل",
|
||||
"pointerOff": "لا تُشِر إلى هذا الزر مرة أخرى",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "قائمة UPG-100 على خريطة واحدة: الشعوب غير المُبشَّرة، مجمّعة في مناطق يمكنك أن تتبنّاها بالصلاة.",
|
||||
"takeaway": "لكل منطقة: ملصق للحائط، وحزمة صلاة للتوزيع، ومجموعة من بطاقات الصلاة. المثال أدناه هو المنطقة التي تضم أكبر عدد من الناس في شعوب لا يعمل أحد بعد على غرس كنائس بينها."
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "هل يمكن الوصول إلى الأشخاص الذين تريد الوصول إليهم عبر الإنترنت؟",
|
||||
"takeaway": "كل بلد يعيش فيه شعب غير مبلَّغ، مصنَّف حسب استخدام الإنترنت والهاتف المحمول المقاس — لتمييز الأرض التي تصلح للوصول الرقمي عن الأماكن التي لن تجد فيها الشاشة أحدًا. الأرقام وطنية وتحمل سنة رصدها؛ «غير معروف» يعني غير مقاس، لا مفترضًا."
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "ماذا يملك الشعب بالفعل بلغته الخاصة؟",
|
||||
"takeaway": "الكتاب المقدس، والتسجيلات الصوتية، والأفلام، وفيديو الكتاب المقدس، والتدريب — وما الذي ما زال ناقصًا. انقر على بلد لعرض شعوبه، أو على شعب لفتح صفحته مع روابط عاملة لكل مورد وُجد. «لم يُعثر على شيء» تعني غير موجود في المجموعات التي فحصها هذا الإصدار، وليست دليلًا على عدم وجوده."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "كل الشعوب في عرض واحد. صفِّ حسب الصلاة أو الشعب أو المنطقة أو الدولة أو اللغة، فتضيق الخريطة والجدول والإجماليات معًا.",
|
||||
"takeaway": "خذ معك الصفوف التي صفّيتها في جدول بيانات."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "الخريطة إلى جانب القائمة. اختر شعبًا على الخريطة فتتبعه القائمة، وضيِّق القائمة فتتبعها الخريطة.",
|
||||
"takeaway": "خذ معك ملصقًا للخريطة كما تراها، جاهزًا للطباعة، وحزمة صلاة لتوزّعها."
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "كل الشعوب غير المُبشَّرة على خريطة واحدة، ملوّنة بحسب ما إذا كان أحد يصلّي من أجلها بعد، وما إذا كانت مخدومة، وما إذا كانت مُتبنّاة.",
|
||||
"takeaway": "شعب لا يصلّي أحد من أجله بعد، ورابط لمشاركته."
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "ابحث عن المجموعات الشعبية غير المبلَّغة حسب المنطقة والدولة.",
|
||||
"takeaway": "افتح دولة لرؤية مجموعاتها، ثم أنشئ ملصقًا وحزمة صلاة ومجموعة بطاقات للطباعة أو المشاركة.",
|
||||
"note": "تُنشأ الملصقات والحزم والبطاقات في متصفحك لكل دولة ما عدا الهند والصين (قريبًا)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/de/mapInfo.json
Normal file
50
library/i18n/locales/de/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "Was kann diese Karte für Sie tun?",
|
||||
"title": "Was kann diese Karte für Sie tun?",
|
||||
"pointer": "Was kann diese Karte?",
|
||||
"close": "Schließen",
|
||||
"takeaway": "Was Sie mitnehmen:",
|
||||
"poster": "Poster",
|
||||
"packet": "Gebetsheft",
|
||||
"cards": "Gebetskarten",
|
||||
"spreadsheet": "Tabelle",
|
||||
"example": "Beispiel",
|
||||
"expandPages": "Alle Seiten anzeigen ({n})",
|
||||
"collapsePages": "Nur die erste Seite anzeigen",
|
||||
"expandDeck": "Alle Karten anzeigen ({n})",
|
||||
"collapseDeck": "Karten stapeln",
|
||||
"moreRows": "Weitere Zeilen in der vollständigen Tabelle: {n}",
|
||||
"openFull": "In voller Größe öffnen",
|
||||
"pointerOff": "Nicht mehr auf diese Schaltfläche hinweisen",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "Die UPG-100-Liste auf einer Karte: unerreichte Volksgruppen, zusammengefasst in Zonen, die Sie im Gebet übernehmen können.",
|
||||
"takeaway": "für jede Zone ein Wandposter, ein Gebetsheft zum Weitergeben und ein Satz Gebetskarten. Das Beispiel unten ist die Zone mit den meisten Menschen in Volksgruppen, unter denen noch niemand Gemeinden gründet."
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "Sind die Menschen, die Sie erreichen wollen, online erreichbar?",
|
||||
"takeaway": "Jedes Land, in dem eine unerreichte Volksgruppe lebt, gestaffelt nach gemessener Internet- und Mobilfunknutzung — damit Sie digital erreichbares Gebiet von Orten unterscheiden können, an denen ein Bildschirm niemanden findet. Die Zahlen sind national und tragen ihr Erhebungsjahr; unbekannt heißt ungemessen, nie angenommen."
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "Was hat eine Volksgruppe bereits in ihrer eigenen Sprache?",
|
||||
"takeaway": "Bibeltext, Audio, Film, Bibelvideo, Schulung — und was noch fehlt. Klicken Sie auf ein Land, um seine Gruppen zu sehen, oder auf eine Gruppe für ihr eigenes Blatt mit funktionierenden Links zu jeder gefundenen Ressource. „Nichts gefunden“ heißt: nicht in den von diesem Build geprüften Quellen enthalten — nie ein Beweis, dass nichts existiert."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Alle Völker in einer Ansicht. Filtern Sie nach Gebet, Volk, Region, Land oder Sprache – Karte, Tabelle und Summen werden gemeinsam eingegrenzt.",
|
||||
"takeaway": "Nehmen Sie die gefilterten Zeilen als Tabelle mit."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "Die Karte neben der Liste. Wählen Sie ein Volk auf der Karte, und die Liste folgt; grenzen Sie die Liste ein, und die Karte folgt.",
|
||||
"takeaway": "Nehmen Sie ein Poster der Karte, wie Sie sie sehen, druckfertig, und ein Gebetspaket zum Verteilen mit."
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "Alle unerreichten Volksgruppen auf einer Karte, eingefärbt danach, ob schon jemand für sie betet, ob sie erreicht werden und ob sie adoptiert sind.",
|
||||
"takeaway": "Eine Volksgruppe, für die noch niemand betet, und ein Link zum Teilen."
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "Finde unerreichte Volksgruppen nach Region und Land.",
|
||||
"takeaway": "Öffnen Sie ein Land, um seine Volksgruppen zu sehen, und erzeugen Sie dann ein Poster, ein Gebetspaket und ein Kartenset zum Drucken oder Teilen.",
|
||||
"note": "Poster, Pakete und Karten entstehen in Ihrem Browser für jedes Land außer Indien und China (in Kürze)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/en/mapInfo.json
Normal file
50
library/i18n/locales/en/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "What can this map do for you?",
|
||||
"title": "What can this map do for you?",
|
||||
"pointer": "What can this map do?",
|
||||
"close": "Close",
|
||||
"takeaway": "What you take away:",
|
||||
"poster": "Poster",
|
||||
"packet": "Prayer packet",
|
||||
"cards": "Prayer cards",
|
||||
"spreadsheet": "Spreadsheet",
|
||||
"example": "Example",
|
||||
"expandPages": "Show all pages ({n})",
|
||||
"collapsePages": "Show the first page only",
|
||||
"expandDeck": "Show all cards ({n})",
|
||||
"collapseDeck": "Stack the cards",
|
||||
"moreRows": "More rows in the full spreadsheet: {n}",
|
||||
"openFull": "Open full size",
|
||||
"pointerOff": "Don't point at this button again",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "The UPG-100 list on one map: unreached people groups, gathered into zones you can adopt in prayer.",
|
||||
"takeaway": "for any zone, a wall poster, a prayer packet to hand out and a deck of prayer cards. The example below is the zone with the most people in groups where no one is planting churches yet."
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "Can the people you want to reach be reached online?",
|
||||
"takeaway": "Every country where an unengaged people group lives, banded by measured internet and mobile use — so you can tell digital-first ground from places where a screen will not find anyone. Figures are national and carry their observation year; unknown means unmeasured, never assumed."
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "What does a people group already have in its own language?",
|
||||
"takeaway": "Scripture, audio, film, Bible video, training — and what is still missing. Click a country to list its groups, or a group for its own sheet with working links to every resource found. “None found” means not present in the pools this build checked, never proof that nothing exists."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Every people group in one view. Filter by prayer, people, region, country or language, and the map, the table and the totals all narrow together.",
|
||||
"takeaway": "Take away the rows you filtered to, as a spreadsheet."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "The map beside the list. Pick a people group on the map and the list follows it; narrow the list and the map follows back.",
|
||||
"takeaway": "Take away a poster of the map as you see it, ready to print, and a prayer packet to hand out."
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "Every unreached people group on one map, coloured by whether anyone is praying for it yet, whether it is engaged, and whether it is adopted.",
|
||||
"takeaway": "A people group that no one is praying for yet, and a link to share it."
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "Find unreached people groups by region and country.",
|
||||
"takeaway": "Open a country to see its groups, then generate a poster, a prayer packet and a card deck to print or share.",
|
||||
"note": "Posters, packets and cards generate in your browser for each country except India and China (coming soon)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/es/mapInfo.json
Normal file
50
library/i18n/locales/es/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "¿Qué puede hacer este mapa por usted?",
|
||||
"title": "¿Qué puede hacer este mapa por usted?",
|
||||
"pointer": "¿Qué hace este mapa?",
|
||||
"close": "Cerrar",
|
||||
"takeaway": "Lo que se lleva:",
|
||||
"poster": "Póster",
|
||||
"packet": "Paquete de oración",
|
||||
"cards": "Tarjetas de oración",
|
||||
"spreadsheet": "Hoja de cálculo",
|
||||
"example": "Ejemplo",
|
||||
"expandPages": "Ver todas las páginas ({n})",
|
||||
"collapsePages": "Ver solo la primera página",
|
||||
"expandDeck": "Ver todas las tarjetas ({n})",
|
||||
"collapseDeck": "Apilar las tarjetas",
|
||||
"moreRows": "Más filas en la hoja de cálculo completa: {n}",
|
||||
"openFull": "Abrir a tamaño completo",
|
||||
"pointerOff": "No volver a señalar este botón",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "La lista UPG-100 en un solo mapa: pueblos no alcanzados, agrupados en zonas que usted puede adoptar en oración.",
|
||||
"takeaway": "para cada zona, un póster para la pared, un paquete de oración para repartir y un mazo de tarjetas de oración. El ejemplo de abajo es la zona con más personas en pueblos donde todavía nadie está plantando iglesias."
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "¿Se puede llegar en línea a las personas que quiere alcanzar?",
|
||||
"takeaway": "Cada país donde vive un pueblo no alcanzado, agrupado por uso medido de internet y móvil, para distinguir el terreno digital de los lugares donde una pantalla no encontrará a nadie. Las cifras son nacionales y llevan su año de observación; desconocido significa no medido, nunca supuesto."
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "¿Qué tiene ya un pueblo en su propio idioma?",
|
||||
"takeaway": "Escritura, audio, película, video bíblico, formación — y qué falta todavía. Haga clic en un país para ver sus pueblos, o en un pueblo para su ficha con enlaces activos a cada recurso encontrado. «No encontrado» significa que no está en los repositorios que esta versión consultó, nunca prueba de que no exista."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Todos los pueblos en una sola vista. Filtre por oración, pueblo, región, país o idioma: el mapa, la tabla y los totales se reducen a la vez.",
|
||||
"takeaway": "Llévese las filas que filtró, en una hoja de cálculo."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "El mapa junto a la lista. Elija un pueblo en el mapa y la lista lo sigue; reduzca la lista y el mapa la sigue.",
|
||||
"takeaway": "Llévese un póster del mapa tal como lo ve, listo para imprimir, y un paquete de oración para repartir."
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "Todos los grupos étnicos no alcanzados en un solo mapa, coloreados según si alguien ya ora por ellos, si están siendo alcanzados y si han sido adoptados.",
|
||||
"takeaway": "Un grupo étnico por el que nadie ora todavía, y un enlace para compartirlo."
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "Encuentra grupos étnicos no alcanzados por región y país.",
|
||||
"takeaway": "Abre un país para ver sus grupos y genera un póster, un paquete de oración y una baraja de tarjetas para imprimir o compartir.",
|
||||
"note": "Los pósteres, paquetes y tarjetas se generan en tu navegador para cada país excepto India y China (próximamente)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/fr/mapInfo.json
Normal file
50
library/i18n/locales/fr/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "Que peut faire cette carte pour vous ?",
|
||||
"title": "Que peut faire cette carte pour vous ?",
|
||||
"pointer": "Que fait cette carte ?",
|
||||
"close": "Fermer",
|
||||
"takeaway": "Ce que vous emportez :",
|
||||
"poster": "Affiche",
|
||||
"packet": "Livret de prière",
|
||||
"cards": "Cartes de prière",
|
||||
"spreadsheet": "Tableur",
|
||||
"example": "Exemple",
|
||||
"expandPages": "Afficher toutes les pages ({n})",
|
||||
"collapsePages": "N'afficher que la première page",
|
||||
"expandDeck": "Afficher toutes les cartes ({n})",
|
||||
"collapseDeck": "Empiler les cartes",
|
||||
"moreRows": "Lignes supplémentaires dans le tableur complet : {n}",
|
||||
"openFull": "Ouvrir en taille réelle",
|
||||
"pointerOff": "Ne plus signaler ce bouton",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "La liste UPG-100 sur une seule carte : les peuples non atteints, regroupés en zones que vous pouvez adopter dans la prière.",
|
||||
"takeaway": "pour chaque zone, une affiche murale, un livret de prière à distribuer et un jeu de cartes de prière. L'exemple ci-dessous est la zone qui compte le plus de personnes dans des peuples où personne n'implante encore d'Églises."
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "Les personnes que vous voulez atteindre sont-elles joignables en ligne ?",
|
||||
"takeaway": "Chaque pays où vit un peuple non atteint, classé selon l'usage mesuré d'internet et du mobile — pour distinguer le terrain numérique des lieux où un écran ne trouvera personne. Les chiffres sont nationaux et portent leur année d'observation ; inconnu signifie non mesuré, jamais supposé."
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "Que possède déjà un peuple dans sa propre langue ?",
|
||||
"takeaway": "Écriture, audio, film, vidéo biblique, formation — et ce qui manque encore. Cliquez sur un pays pour lister ses peuples, ou sur un peuple pour sa fiche avec des liens actifs vers chaque ressource trouvée. « Rien trouvé » signifie absent des fonds vérifiés par cette version, jamais une preuve que rien n'existe."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Tous les peuples dans une seule vue. Filtrez par prière, peuple, région, pays ou langue : la carte, le tableau et les totaux se réduisent ensemble.",
|
||||
"takeaway": "Emportez les lignes filtrées sous forme de tableur."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "La carte à côté de la liste. Choisissez un peuple sur la carte et la liste le suit ; réduisez la liste et la carte suit.",
|
||||
"takeaway": "Emportez une affiche de la carte telle que vous la voyez, prête à imprimer, et un dossier de prière à distribuer."
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "Tous les peuples non atteints sur une seule carte, colorés selon que quelqu'un prie déjà pour eux, qu'ils sont engagés et qu'ils sont adoptés.",
|
||||
"takeaway": "Un peuple pour lequel personne ne prie encore, et un lien pour le partager."
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "Trouvez des peuples non atteints par région et par pays.",
|
||||
"takeaway": "Ouvrez un pays pour voir ses groupes, puis générez une affiche, un dossier de prière et un jeu de cartes à imprimer ou à partager.",
|
||||
"note": "Affiches, dossiers et cartes sont générés dans votre navigateur pour chaque pays sauf l'Inde et la Chine (bientôt disponible)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/hi/mapInfo.json
Normal file
50
library/i18n/locales/hi/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "यह नक्शा आपके लिए क्या कर सकता है?",
|
||||
"title": "यह नक्शा आपके लिए क्या कर सकता है?",
|
||||
"pointer": "यह नक्शा क्या करता है?",
|
||||
"close": "बंद करें",
|
||||
"takeaway": "आपको क्या मिलता है:",
|
||||
"poster": "पोस्टर",
|
||||
"packet": "प्रार्थना पुस्तिका",
|
||||
"cards": "प्रार्थना कार्ड",
|
||||
"spreadsheet": "स्प्रेडशीट",
|
||||
"example": "उदाहरण",
|
||||
"expandPages": "सभी पृष्ठ दिखाएँ ({n})",
|
||||
"collapsePages": "केवल पहला पृष्ठ दिखाएँ",
|
||||
"expandDeck": "सभी कार्ड दिखाएँ ({n})",
|
||||
"collapseDeck": "कार्ड एक साथ रखें",
|
||||
"moreRows": "पूरी स्प्रेडशीट में और पंक्तियाँ: {n}",
|
||||
"openFull": "पूरे आकार में खोलें",
|
||||
"pointerOff": "इस बटन की ओर फिर से इशारा न करें",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "UPG-100 सूची एक ही नक्शे पर: अपहुँचे जन-समूह, क्षेत्रों में बाँटे गए, जिन्हें आप प्रार्थना में अपना सकते हैं।",
|
||||
"takeaway": "हर क्षेत्र के लिए दीवार का एक पोस्टर, बाँटने के लिए एक प्रार्थना पुस्तिका और प्रार्थना कार्डों का एक सेट। नीचे का उदाहरण वह क्षेत्र है जहाँ ऐसे जन-समूहों में सबसे अधिक लोग हैं जिनके बीच अभी कोई कलीसिया स्थापित नहीं कर रहा है।"
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "जिन लोगों तक आप पहुँचना चाहते हैं, क्या उन तक ऑनलाइन पहुँचा जा सकता है?",
|
||||
"takeaway": "हर वह देश जहाँ कोई अनपहुँचा जनसमूह रहता है, मापी गई इंटरनेट और मोबाइल उपयोग के अनुसार वर्गीकृत — ताकि आप डिजिटल रूप से पहुँच योग्य क्षेत्र को उन स्थानों से अलग कर सकें जहाँ स्क्रीन किसी को नहीं मिलेगी। आँकड़े राष्ट्रीय हैं और अपने अवलोकन वर्ष के साथ आते हैं; अज्ञात का अर्थ है बिना मापा, कभी अनुमानित नहीं।"
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "किसी जनसमूह के पास उसकी अपनी भाषा में पहले से क्या है?",
|
||||
"takeaway": "पवित्रशास्त्र, ऑडियो, फ़िल्म, बाइबल वीडियो, प्रशिक्षण — और क्या अब भी नहीं है। किसी देश पर क्लिक करके उसके जनसमूह देखें, या किसी जनसमूह पर क्लिक करके उसकी अपनी शीट खोलें, जिसमें मिले हर संसाधन के काम करने वाले लिंक हैं। “कुछ नहीं मिला” का अर्थ है इस बिल्ड द्वारा जाँचे गए स्रोतों में उपस्थित नहीं — यह प्रमाण नहीं कि कुछ मौजूद ही नहीं।"
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "सभी जनसमूह एक ही दृश्य में। प्रार्थना, जनसमूह, क्षेत्र, देश या भाषा से छाँटें — नक्शा, तालिका और कुल एक साथ सीमित होते हैं।",
|
||||
"takeaway": "छाँटी गई पंक्तियाँ स्प्रेडशीट के रूप में साथ ले जाएँ।"
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "सूची के साथ नक्शा। नक्शे पर एक जनसमूह चुनें और सूची उसका अनुसरण करती है; सूची सीमित करें और नक्शा अनुसरण करता है।",
|
||||
"takeaway": "नक्शे का पोस्टर जैसा आप देखते हैं, छपाई के लिए तैयार, और बाँटने के लिए प्रार्थना पैकेट साथ ले जाएँ।"
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "सभी अपहुँचे जन-समूह एक ही नक्शे पर, इस आधार पर रंगे हुए कि क्या कोई उनके लिए अभी प्रार्थना कर रहा है, क्या उनके बीच काम हो रहा है, और क्या उन्हें गोद लिया गया है।",
|
||||
"takeaway": "एक जन-समूह जिसके लिए अभी कोई प्रार्थना नहीं कर रहा, और उसे साझा करने का लिंक।"
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "क्षेत्र और देश के अनुसार अगम्य लोगों के समूह खोजें।",
|
||||
"takeaway": "किसी देश को खोलकर उसके जनसमूह देखें, फिर छापने या साझा करने के लिए पोस्टर, प्रार्थना पैकेट और कार्ड सेट बनाएँ।",
|
||||
"note": "पोस्टर, पैकेट और कार्ड आपके ब्राउज़र में भारत और चीन को छोड़कर हर देश के लिए बनते हैं (शीघ्र आ रहा है)।"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/it/mapInfo.json
Normal file
50
library/i18n/locales/it/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "Che cosa può fare questa mappa per te?",
|
||||
"title": "Che cosa può fare questa mappa per te?",
|
||||
"pointer": "Che cosa fa questa mappa?",
|
||||
"close": "Chiudi",
|
||||
"takeaway": "Che cosa ottieni:",
|
||||
"poster": "Poster",
|
||||
"packet": "Libretto di preghiera",
|
||||
"cards": "Carte di preghiera",
|
||||
"spreadsheet": "Foglio di calcolo",
|
||||
"example": "Esempio",
|
||||
"expandPages": "Mostra tutte le pagine ({n})",
|
||||
"collapsePages": "Mostra solo la prima pagina",
|
||||
"expandDeck": "Mostra tutte le carte ({n})",
|
||||
"collapseDeck": "Impila le carte",
|
||||
"moreRows": "Altre righe nel foglio di calcolo completo: {n}",
|
||||
"openFull": "Apri a grandezza intera",
|
||||
"pointerOff": "Non indicare più questo pulsante",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "La lista UPG-100 su un'unica mappa: popoli non raggiunti, raggruppati in zone che puoi adottare nella preghiera.",
|
||||
"takeaway": "per ogni zona, un poster da parete, un libretto di preghiera da distribuire e un mazzo di carte di preghiera. L'esempio qui sotto è la zona con più persone in popoli dove nessuno sta ancora fondando chiese."
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "Le persone che vuoi raggiungere sono raggiungibili online?",
|
||||
"takeaway": "Ogni paese in cui vive un popolo non raggiunto, suddiviso per uso misurato di internet e mobile — per distinguere il terreno digitale dai luoghi in cui uno schermo non troverà nessuno. Le cifre sono nazionali e riportano l'anno di rilevazione; sconosciuto significa non misurato, mai presunto."
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "Che cosa ha già un popolo nella propria lingua?",
|
||||
"takeaway": "Scrittura, audio, film, video biblico, formazione — e che cosa manca ancora. Clicca su un paese per elencarne i popoli, o su un popolo per la sua scheda con collegamenti attivi a ogni risorsa trovata. «Nessuna risorsa trovata» significa non presente negli archivi controllati da questa build, mai una prova che non esista nulla."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Tutti i popoli in un'unica vista. Filtra per preghiera, popolo, regione, paese o lingua: mappa, tabella e totali si restringono insieme.",
|
||||
"takeaway": "Porta con te le righe filtrate, come foglio di calcolo."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "La mappa accanto all'elenco. Scegli un popolo sulla mappa e l'elenco lo segue; restringi l'elenco e la mappa lo segue.",
|
||||
"takeaway": "Porta con te un poster della mappa così come la vedi, pronto da stampare, e un pacchetto di preghiera da distribuire."
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "Tutti i popoli non raggiunti su un'unica mappa, colorati secondo se qualcuno prega già per loro, se sono raggiunti e se sono adottati.",
|
||||
"takeaway": "Un popolo per cui nessuno prega ancora, e un link per condividerlo."
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "Trova i popoli non raggiunti per regione e paese.",
|
||||
"takeaway": "Apri un Paese per vederne i gruppi, poi genera un poster, un pacchetto di preghiera e un mazzo di schede da stampare o condividere.",
|
||||
"note": "Poster, pacchetti e schede vengono generati nel tuo browser per ogni Paese tranne India e Cina (prossimamente)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/pt/mapInfo.json
Normal file
50
library/i18n/locales/pt/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "O que este mapa pode fazer por você?",
|
||||
"title": "O que este mapa pode fazer por você?",
|
||||
"pointer": "O que este mapa faz?",
|
||||
"close": "Fechar",
|
||||
"takeaway": "O que você leva:",
|
||||
"poster": "Pôster",
|
||||
"packet": "Pacote de oração",
|
||||
"cards": "Cartões de oração",
|
||||
"spreadsheet": "Planilha",
|
||||
"example": "Exemplo",
|
||||
"expandPages": "Mostrar todas as páginas ({n})",
|
||||
"collapsePages": "Mostrar só a primeira página",
|
||||
"expandDeck": "Mostrar todos os cartões ({n})",
|
||||
"collapseDeck": "Empilhar os cartões",
|
||||
"moreRows": "Mais linhas na planilha completa: {n}",
|
||||
"openFull": "Abrir em tamanho real",
|
||||
"pointerOff": "Não apontar mais para este botão",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "A lista UPG-100 em um só mapa: povos não alcançados, agrupados em zonas que você pode adotar em oração.",
|
||||
"takeaway": "para cada zona, um pôster de parede, um pacote de oração para distribuir e um baralho de cartões de oração. O exemplo abaixo é a zona com mais pessoas em povos onde ninguém ainda está plantando igrejas."
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "As pessoas que você quer alcançar podem ser alcançadas online?",
|
||||
"takeaway": "Cada país onde vive um povo não alcançado, agrupado por uso medido de internet e telemóvel — para distinguir o terreno digital dos lugares onde um ecrã não encontrará ninguém. Os números são nacionais e trazem o ano de observação; desconhecido significa não medido, nunca presumido."
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "O que um povo já tem na sua própria língua?",
|
||||
"takeaway": "Escritura, áudio, filme, vídeo bíblico, formação — e o que ainda falta. Clique num país para listar os seus povos, ou num povo para a sua ficha com ligações activas a cada recurso encontrado. «Nada encontrado» significa ausente dos acervos que esta versão consultou, nunca prova de que nada exista."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Todos os povos numa só vista. Filtre por oração, povo, região, país ou idioma: o mapa, a tabela e os totais reduzem-se em conjunto.",
|
||||
"takeaway": "Leve as linhas que filtrou, como folha de cálculo."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "O mapa ao lado da lista. Escolha um povo no mapa e a lista segue-o; reduza a lista e o mapa segue.",
|
||||
"takeaway": "Leve um pôster do mapa tal como o vê, pronto a imprimir, e um pacote de oração para distribuir."
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "Todos os povos não alcançados em um só mapa, coloridos conforme alguém já ora por eles, se estão sendo alcançados e se foram adotados.",
|
||||
"takeaway": "Um povo pelo qual ninguém ora ainda, e um link para compartilhá-lo."
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "Encontre grupos étnicos não alcançados por região e país.",
|
||||
"takeaway": "Abra um país para ver os seus grupos e gere um cartaz, um pacote de oração e um baralho de cartões para imprimir ou partilhar.",
|
||||
"note": "Cartazes, pacotes e cartões são gerados no seu navegador para cada país exceto a Índia e a China (em breve)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/ro/mapInfo.json
Normal file
50
library/i18n/locales/ro/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "Ce poate face această hartă pentru tine?",
|
||||
"title": "Ce poate face această hartă pentru tine?",
|
||||
"pointer": "Ce face această hartă?",
|
||||
"close": "Închide",
|
||||
"takeaway": "Ce primești:",
|
||||
"poster": "Poster",
|
||||
"packet": "Pachet de rugăciune",
|
||||
"cards": "Cartonașe de rugăciune",
|
||||
"spreadsheet": "Foaie de calcul",
|
||||
"example": "Exemplu",
|
||||
"expandPages": "Arată toate paginile ({n})",
|
||||
"collapsePages": "Arată doar prima pagină",
|
||||
"expandDeck": "Arată toate cartonașele ({n})",
|
||||
"collapseDeck": "Strânge cartonașele",
|
||||
"moreRows": "Rânduri suplimentare în foaia de calcul completă: {n}",
|
||||
"openFull": "Deschide la dimensiune completă",
|
||||
"pointerOff": "Nu mai indica acest buton",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "Lista UPG-100 pe o singură hartă: popoare neatinse, grupate în zone pe care le poți adopta în rugăciune.",
|
||||
"takeaway": "pentru fiecare zonă, un poster de perete, un pachet de rugăciune de împărțit și un set de cartonașe de rugăciune. Exemplul de mai jos este zona cu cei mai mulți oameni din popoare în care nimeni nu plantează încă biserici."
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "Pot fi atinse online persoanele pe care vrei să le ajungi?",
|
||||
"takeaway": "Fiecare țară în care trăiește un popor neatins, grupat după utilizarea măsurată a internetului și a telefoniei mobile — ca să deosebești terenul digital de locurile unde un ecran nu va găsi pe nimeni. Cifrele sunt naționale și poartă anul observației; necunoscut înseamnă nemăsurat, niciodată presupus."
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "Ce are deja un popor în propria sa limbă?",
|
||||
"takeaway": "Scriptură, audio, film, video biblic, instruire — și ce lipsește încă. Apasă pe o țară pentru a-i afișa popoarele, sau pe un popor pentru fișa sa, cu legături active către fiecare resursă găsită. „Nimic găsit” înseamnă absent din colecțiile verificate de această versiune, niciodată o dovadă că nu există nimic."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Toate popoarele într-o singură vedere. Filtrează după rugăciune, popor, regiune, țară sau limbă — harta, tabelul și totalurile se restrâng împreună.",
|
||||
"takeaway": "Ia cu tine rândurile filtrate, ca foaie de calcul."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "Harta lângă listă. Alege un popor pe hartă și lista îl urmează; restrânge lista și harta o urmează.",
|
||||
"takeaway": "Ia cu tine un poster al hărții așa cum o vezi, gata de tipărit, și un pachet de rugăciune de împărțit."
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "Toate grupurile etnice neatinse pe o singură hartă, colorate după cum cineva se roagă deja pentru ele, dacă sunt atinse și dacă sunt adoptate.",
|
||||
"takeaway": "Un grup etnic pentru care nimeni nu se roagă încă, și un link pentru a-l distribui."
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "Găsește popoarele neatinse după regiune și țară.",
|
||||
"takeaway": "Deschide o țară pentru a-i vedea grupurile, apoi generează un afiș, un pachet de rugăciune și un set de cartonașe pentru tipărire sau partajare.",
|
||||
"note": "Afișele, pachetele și cartonașele se generează în browserul tău pentru fiecare țară în afară de India și China (în curând)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/ru/mapInfo.json
Normal file
50
library/i18n/locales/ru/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "Чем эта карта может быть вам полезна?",
|
||||
"title": "Чем эта карта может быть вам полезна?",
|
||||
"pointer": "Что умеет эта карта?",
|
||||
"close": "Закрыть",
|
||||
"takeaway": "Что вы получаете:",
|
||||
"poster": "Плакат",
|
||||
"packet": "Молитвенный буклет",
|
||||
"cards": "Молитвенные карточки",
|
||||
"spreadsheet": "Таблица",
|
||||
"example": "Пример",
|
||||
"expandPages": "Показать все страницы ({n})",
|
||||
"collapsePages": "Показать только первую страницу",
|
||||
"expandDeck": "Показать все карточки ({n})",
|
||||
"collapseDeck": "Сложить карточки в стопку",
|
||||
"moreRows": "Ещё строк в полной таблице: {n}",
|
||||
"openFull": "Открыть в полном размере",
|
||||
"pointerOff": "Больше не указывать на эту кнопку",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "Список UPG-100 на одной карте: недостигнутые народы, объединённые в зоны, которые вы можете взять на молитвенное попечение.",
|
||||
"takeaway": "для каждой зоны — настенный плакат, молитвенный буклет для раздачи и набор молитвенных карточек. Ниже показана зона, где больше всего людей в народах, среди которых пока никто не основывает церкви."
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "Можно ли связаться с нужными людьми через интернет?",
|
||||
"takeaway": "Каждая страна, где живёт неохваченный народ, сгруппированная по измеренному использованию интернета и мобильной связи — чтобы отличить цифровую почву от мест, где экран никого не найдёт. Цифры национальные и указывают год наблюдения; «неизвестно» означает не измерено, а не предположено."
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "Что у народа уже есть на его родном языке?",
|
||||
"takeaway": "Писание, аудио, фильм, библейское видео, обучение — и чего ещё не хватает. Нажмите на страну, чтобы увидеть её народы, или на народ, чтобы открыть его карточку с рабочими ссылками на каждый найденный ресурс. «Ничего не найдено» означает отсутствие в проверенных этой сборкой источниках, а не доказательство, что ничего не существует."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Все народы в одном представлении. Фильтруйте по молитве, народу, региону, стране или языку — карта, таблица и итоги сужаются вместе.",
|
||||
"takeaway": "Заберите отфильтрованные строки в виде таблицы."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "Карта рядом со списком. Выберите народ на карте — список последует за ним; сузьте список — карта последует за ним.",
|
||||
"takeaway": "Заберите плакат карты в том виде, как вы её видите, готовый к печати, и молитвенный пакет для раздачи."
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "Все неохваченные народы на одной карте, окрашенные по тому, молится ли уже кто-нибудь о них, ведётся ли среди них служение и усыновлены ли они.",
|
||||
"takeaway": "Народ, о котором ещё никто не молится, и ссылка, чтобы поделиться им."
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "Найдите недостигнутые группы людей по регионам и странам.",
|
||||
"takeaway": "Откройте страну, чтобы увидеть её народности, затем создайте плакат, молитвенный пакет и набор карточек для печати или отправки.",
|
||||
"note": "Плакаты, пакеты и карточки создаются в вашем браузере для каждой страны, кроме Индии и Китая (скоро)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
library/i18n/locales/zh/mapInfo.json
Normal file
50
library/i18n/locales/zh/mapInfo.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"open": "这张地图能为你做什么?",
|
||||
"title": "这张地图能为你做什么?",
|
||||
"pointer": "这张地图能做什么?",
|
||||
"close": "关闭",
|
||||
"takeaway": "你能带走的:",
|
||||
"poster": "海报",
|
||||
"packet": "祷告手册",
|
||||
"cards": "祷告卡",
|
||||
"spreadsheet": "电子表格",
|
||||
"example": "示例",
|
||||
"expandPages": "显示全部页面({n})",
|
||||
"collapsePages": "只显示第一页",
|
||||
"expandDeck": "显示全部卡片({n})",
|
||||
"collapseDeck": "把卡片叠起来",
|
||||
"moreRows": "完整电子表格中的更多行:{n}",
|
||||
"openFull": "以完整尺寸打开",
|
||||
"pointerOff": "不再指向这个按钮",
|
||||
"tab": {
|
||||
"upg-100": {
|
||||
"lead": "UPG-100 名单尽在一张地图上:未得之民按区域分组,你可以在祷告中认领一个区域。",
|
||||
"takeaway": "每个区域都有一张墙面海报、一本可分发的祷告手册和一套祷告卡。下面的示例是这样一个区域:在尚无人植堂的族群中,这里的人数最多。"
|
||||
},
|
||||
"cities-internet": {
|
||||
"lead": "你想触及的人群能在线上被触及吗?",
|
||||
"takeaway": "每一个有未得之民居住的国家,按实测的互联网和移动使用率分档——让你分辨哪些是数字可达之地,哪些地方屏幕找不到任何人。数据为全国性并标注观测年份;“未知”表示未测量,绝非假定。"
|
||||
},
|
||||
"gospel-resources": {
|
||||
"lead": "一个族群用自己的语言已经拥有什么?",
|
||||
"takeaway": "圣经、音频、影片、圣经视频、门训——以及还缺什么。点击国家可列出其族群,点击族群可打开其专页,内含每项已找到资源的有效链接。“未找到”表示不在此版本所查的资源库中,绝不等于证明它不存在。"
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "在一个视图中查看所有民族。按祷告、民族、区域、国家或语言筛选,地图、表格和总计会一同收窄。",
|
||||
"takeaway": "把筛选后的行作为电子表格带走。"
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "地图与列表并排。在地图上选择一个民族,列表随之跟随;缩小列表范围,地图也随之跟随。",
|
||||
"takeaway": "带走一张你所见地图的海报,可直接打印,以及一份可分发的祷告手册。"
|
||||
},
|
||||
"prayer-map": {
|
||||
"lead": "所有未得之民群体都在一张地图上,按是否已有人为他们祷告、是否已有人在其中事工、是否已被认领来着色。",
|
||||
"takeaway": "一个还没有人为之祷告的群体,以及一个分享它的链接。"
|
||||
},
|
||||
"regions-dashboard": {
|
||||
"lead": "按区域和国家查找未得之民。",
|
||||
"takeaway": "打开一个国家查看其族群,然后生成海报、祷告册和卡片集,可打印或分享。",
|
||||
"note": "海报、祷告册和卡片在您的浏览器中生成,适用于除印度和中国以外的每个国家(即将推出)。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -238,6 +238,7 @@
|
|||
"check:popup-href-escaped": "node internal/check-popup-href-escaped.mjs",
|
||||
"check:popup-pan-controls": "node internal/check-popup-pan-clears-controls.mjs",
|
||||
"check:country-map-refetch": "node internal/check-country-map-refetch.mjs",
|
||||
"check:pane-config-reforward": "node internal/check-pane-config-reforward.mjs",
|
||||
"check:control-inset": "node internal/check-control-inset-measured.mjs",
|
||||
"check:viewport-meta": "node internal/check-viewport-meta.mjs",
|
||||
"check:viewport-meta:self-test": "VIEWPORT_META_SELFTEST=1 node internal/check-viewport-meta.mjs",
|
||||
|
|
@ -341,7 +342,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:country-map-refetch 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",
|
||||
"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:country-map-refetch 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",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
* wall's 'show-on-map', and it keeps each pane under its own ProfileLoader/Pinia.
|
||||
*/
|
||||
import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { usePaneConfig } from '@map/composables/usePaneConfig.js'
|
||||
import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pane-wrapper.js'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { SHADOW_BOX_SIZING_RESET } from './src/shadowReset.js'
|
||||
|
|
@ -113,16 +114,27 @@ let offScroll = null
|
|||
// below), and this was the one genuinely missing.
|
||||
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'prayBaseUrl', 'tokenUrl', 'dataSource', 'locale', 'lang', 'languageCode', 'isDark', 'qa', 'pinHint', 'urlSync', 'provider', 'list']
|
||||
|
||||
function paneElement(pane) {
|
||||
const el = document.createElement(TAG)
|
||||
// ★ SPLIT OUT SO IT CAN BE RE-EVALUATED. This reads cfg.value and spec.value, so calling it again
|
||||
// after the outer profile-config changes yields the NEW pane config. It used to be inline in
|
||||
// paneElement, which ran once per pane in onMounted and never again -- that is the whole defect:
|
||||
// the pane is handed a JSON STRING, so a later locale change reached this element and stopped.
|
||||
// stage is the element the panes are appended UNDER, in the same tree as them -- contains() does
|
||||
// not cross a shadow boundary, so the host must not be the custom element itself.
|
||||
const paneConfig = usePaneConfig(() => stage.value)
|
||||
|
||||
function paneConfigFor(pane) {
|
||||
const host = cfg.value || {}
|
||||
const inherited = {}
|
||||
for (const k of INHERITED_PAGE_KEYS) {
|
||||
if (host[k] !== undefined && host[k] !== null && host[k] !== '') inherited[k] = host[k]
|
||||
}
|
||||
if (inherited.locale === undefined && inherited.lang !== undefined) inherited.locale = inherited.lang
|
||||
const pcfg = { ...inherited, ...spec.value.config, ...pane.config }
|
||||
el.setAttribute('profile-config', JSON.stringify(pcfg))
|
||||
return { ...inherited, ...spec.value.config, ...pane.config }
|
||||
}
|
||||
|
||||
function paneElement(pane) {
|
||||
const el = document.createElement(TAG)
|
||||
el.setAttribute('profile-config', JSON.stringify(paneConfigFor(pane)))
|
||||
return el
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +172,10 @@ onMounted(() => {
|
|||
if (!column) return
|
||||
for (const pane of column.panes) {
|
||||
if (!pane.config) continue
|
||||
column.mountEl(pane.id)?.appendChild(paneElement(pane))
|
||||
const el = paneElement(pane)
|
||||
column.mountEl(pane.id)?.appendChild(el)
|
||||
// Keep this pane's profile-config in step with the outer element's for the rest of the session.
|
||||
paneConfig.register(el, () => paneConfigFor(pane))
|
||||
}
|
||||
// Tabs are a readout as well as a control — follow the user's scroll.
|
||||
offScroll = column.onScroll(() => {
|
||||
|
|
|
|||
|
|
@ -1126,7 +1126,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6",
|
||||
"integrity": "sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -1149,8 +1149,8 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- 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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" 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<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" 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<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1166,7 +1166,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1182,8 +1182,8 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- 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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" 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<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" 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<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1126,7 +1126,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6",
|
||||
"integrity": "sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -1149,8 +1149,8 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- 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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" 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<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" 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<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1166,7 +1166,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1182,8 +1182,8 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- 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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" 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<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" 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<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -61,7 +61,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-+u2vDSnYni3VeNKSYdNWfw5bjKtriFjik/k10lugA0nw6Ozfi/vKUYp3a9sbMAp6" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-YNNxkm78b9Z8oKetaufWw+ajPKAEqk89gSrY5imw8fbVMMAyVvg6hU+9E+osig8s" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue