Merge remote-tracking branch 'origin/development' into prayer-progress-plugin/pin-opacity
This commit is contained in:
commit
78a461588d
26 changed files with 509 additions and 129 deletions
|
|
@ -112,6 +112,45 @@ ok(!noCountry.length && resolveTerritory({ shapeName: 'Mahārāshtra' }).country
|
||||||
ok(!disagree.length, `Copy's regroup warning disagrees with apply-grouping: ${disagree.join('; ')}`,
|
ok(!disagree.length, `Copy's regroup warning disagrees with apply-grouping: ${disagree.join('; ')}`,
|
||||||
`the regroup warning matches apply-grouping's own verdict in all ${Object.keys(REGROUP_CASES).length} cases (India, China, countries, and the three it refuses)`)
|
`the regroup warning matches apply-grouping's own verdict in all ${Object.keys(REGROUP_CASES).length} cases (India, China, countries, and the three it refuses)`)
|
||||||
|
|
||||||
|
// ── A4. A STALLED people-group load can never hang Copy ─────────────────────────────────────────────────
|
||||||
|
// Copy races the people-group detection against COPY_WAIT_MS through settleWithin(); past it, it copies the territory
|
||||||
|
// lines alone (they need no network) and says why. The self-test: a promise that NEVER settles must still resolve to
|
||||||
|
// the marker in time; a value passes through; a failure still rejects (its own message is kept).
|
||||||
|
{
|
||||||
|
const MARK = {}, t0 = Date.now()
|
||||||
|
// The gate's own backstop: if settleWithin waited for ever, this reads 'GATE-TIMEOUT' and FAILS by name.
|
||||||
|
const stalled = await Promise.race([X.settleWithin(new Promise(() => {}), 60, MARK), new Promise((r) => setTimeout(() => r('GATE-TIMEOUT'), 2000))])
|
||||||
|
const quick = await X.settleWithin(Promise.resolve('v'), 1000, MARK)
|
||||||
|
const failed = await X.settleWithin(Promise.reject(new Error('down')), 1000, MARK).then(() => 'resolved', (e) => e.message)
|
||||||
|
ok(stalled === MARK && Date.now() - t0 < 1000 && quick === 'v' && failed === 'down',
|
||||||
|
`settleWithin: a stalled source gave ${stalled === MARK ? 'the marker' : JSON.stringify(stalled)}, a value gave ${quick}, a failure gave ${failed}`,
|
||||||
|
'a NEVER-settling people-group load still lets Copy settle in time (and a value / a failure pass through)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── A5. A FAILED load never shows the reader a raw browser message ─────────────────────────────────────
|
||||||
|
// The second Copy after the 30 s abort flashed "signal is aborted without reason". The self-test aborts a REAL fetch
|
||||||
|
// (the signal is aborted before the request starts, so nothing is sent) and makes a real one fail (a refused
|
||||||
|
// localhost port, bounded at 2 s); both must read as the keyed stall text. A keyed error keeps its own text. The NEGATIVE CONTROL is
|
||||||
|
// the old path, errorText without asReaderError, which must still show the raw message, so this test can see the defect.
|
||||||
|
{
|
||||||
|
const { keyedError, errorText } = await import(pathToFileURL(P('library/i18n/keyedError.js')).href)
|
||||||
|
const en = JSON.parse(readFileSync(P('library/i18n/locales/en/upgMap.json'), 'utf8'))
|
||||||
|
const tr = (k, p = {}) => { const v = k.startsWith('upgMap.') ? en[k.slice(7)] : undefined; return v == null ? k : v.replace(/\{(\w+)\}/g, (m, x) => (p[x] != null ? String(p[x]) : m)) }
|
||||||
|
const stillLoading = () => keyedError('upgMap.peopleStillLoading', 'the people groups are still loading, try again in a moment')
|
||||||
|
const aborted = await fetch('http://127.0.0.1:9/', { signal: AbortSignal.abort() }).then(() => null, (e) => e)
|
||||||
|
// Bounded: a host running the discard service ACCEPTS port 9 and would hang; a timeout is a failure too.
|
||||||
|
const refused = await fetch('http://127.0.0.1:9/', { signal: AbortSignal.timeout(2000) }).then(() => null, (e) => e)
|
||||||
|
const keyed = keyedError('upgMap.selectionAdm1NoData', 'no people-group data for {names}', { names: 'Yunnan' })
|
||||||
|
const shown = (e) => errorText(X.asReaderError(e, stillLoading), tr)
|
||||||
|
const want = en.peopleStillLoading
|
||||||
|
ok(aborted && aborted.name === 'AbortError' && /abort/i.test(errorText(aborted, tr)),
|
||||||
|
`NEGATIVE CONTROL: an aborted fetch gave ${aborted && aborted.name} "${aborted && errorText(aborted, tr)}" on the old path, so the test cannot see the defect`,
|
||||||
|
'negative control: on the old path an aborted fetch shows the raw "' + (aborted ? errorText(aborted, tr) : '') + '"')
|
||||||
|
ok(shown(aborted) === want && refused && shown(refused) === want && shown(keyed) === tr('upgMap.selectionAdm1NoData', { names: 'Yunnan' }),
|
||||||
|
`a failed load reads: aborted "${shown(aborted)}", refused "${refused && shown(refused)}", keyed "${shown(keyed)}"`,
|
||||||
|
'an aborted or refused people-group fetch reads as the stall text; a keyed error keeps its own')
|
||||||
|
}
|
||||||
|
|
||||||
// ── A6. A STATE-ONLY selection titles its Prayer list / Card deck ───────────────────────────────────────
|
// ── A6. A STATE-ONLY selection titles its Prayer list / Card deck ───────────────────────────────────────
|
||||||
// Development threw "Reduce of empty array" for a state-only selection: the title named zones and countries only, so
|
// Development threw "Reduce of empty array" for a state-only selection: the title named zones and countries only, so
|
||||||
// a selection of Gujarāt alone had NO names. On the REAL served table: Gujarāt alone is titled "Gujarāt"; a state +
|
// a selection of Gujarāt alone had NO names. On the REAL served table: Gujarāt alone is titled "Gujarāt"; a state +
|
||||||
|
|
@ -180,8 +219,13 @@ const wired = {
|
||||||
'the Copy button': /@click="copySelection">\{\{ t\('upgMap\.copySelection'\) \}\}/.test(vue),
|
'the Copy button': /@click="copySelection">\{\{ t\('upgMap\.copySelection'\) \}\}/.test(vue),
|
||||||
'the Export CSV button': /@click="exportSelectionCsv">/.test(vue) && vue.includes("t('upgMap.exportCsv')"),
|
'the Export CSV button': /@click="exportSelectionCsv">/.test(vue) && vue.includes("t('upgMap.exportCsv')"),
|
||||||
'the prefetch on batch mode': /if \(next\) selectionRecordsLoad\(\)/.test(vue),
|
'the prefetch on batch mode': /if \(next\) selectionRecordsLoad\(\)/.test(vue),
|
||||||
'one detection for every action': (vue.match(/await selectionPeople\(\)/g) || []).length >= 2 && /selectionPeople\(\)\.then/.test(vue),
|
'one detection for every action': (vue.match(/await selectionPeople\(\)/g) || []).length >= 2 && /settleWithin\(selectionPeople\(\)/.test(vue),
|
||||||
// India's states resolve through the served table (library/data/indiaStates.js), fetched only once a state is selected
|
// India's states resolve through the served table (library/data/indiaStates.js), fetched only once a state is selected
|
||||||
|
'Copy waits at most COPY_WAIT_MS for the people groups': /settleWithin\(selectionPeople\(\), COPY_WAIT_MS, STALLED\)/.test(vue) && /if \(p === STALLED\) \{ note = t\('upgMap\.peopleStillLoading'\); return selectionClipboard\(sel, \{ color \}\); \}/.test(vue),
|
||||||
|
'the records fetch times out and leaves the cache': /setTimeout\(\(\) => ctrl\.abort\(\), RECORDS_TIMEOUT_MS\)/.test(vue) && /signal: ctrl \? ctrl\.signal : undefined/.test(vue),
|
||||||
|
'every failed load reads as the stall, never a raw message': /const readerError = \(e\) => asReaderError\(e, stillLoading\)/.test(vue)
|
||||||
|
&& /\}\)\(\)\.catch\(\(e\) => \{ throw readerError\(e\); \}\);/.test(vue)
|
||||||
|
&& /note = errorText\(readerError\(e\), t\)/.test(vue) && /csvFailed', \{ reason: errorText\(readerError\(e\), t\)/.test(vue),
|
||||||
// In batch mode a tap on a pin opens NO detail sheet (it covered the whole HUD on a phone); the tapestry
|
// In batch mode a tap on a pin opens NO detail sheet (it covered the whole HUD on a phone); the tapestry
|
||||||
// listener under it toggles the territory. The real-tap proof is a browser check; this pins the guard in the source.
|
// listener under it toggles the territory. The real-tap proof is a browser check; this pins the guard in the source.
|
||||||
'a pin tap in batch mode opens no sheet': /map\.on\('click', 'upg-pins-hit', \(e\) => \{[\s\S]{0,700}?if \(batchMode\.value\) return\s*\n\s*const f = e && e\.features/.test(vue),
|
'a pin tap in batch mode opens no sheet': /map\.on\('click', 'upg-pins-hit', \(e\) => \{[\s\S]{0,700}?if \(batchMode\.value\) return\s*\n\s*const f = e && e\.features/.test(vue),
|
||||||
|
|
@ -205,7 +249,7 @@ ok(!missingWires.length, `the HUD is missing: ${missingWires.join(', ')}`, 'the
|
||||||
// ── D. the strings, ×11 ──────────────────────────────────────────────────────────────────────────
|
// ── D. the strings, ×11 ──────────────────────────────────────────────────────────────────────────
|
||||||
const LANGS = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh']
|
const LANGS = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh']
|
||||||
const KEYS = {
|
const KEYS = {
|
||||||
upgMap: ['copySelection', 'exportCsv', 'copyNotRegroupable', 'selectionColourGroup', 'selectionColour', 'customColour', 'copiedSelection', 'copiedTerritoriesOnly', 'csvSaved', 'csvFailed'] }
|
upgMap: ['copySelection', 'exportCsv', 'copyNotRegroupable', 'peopleStillLoading', 'selectionColourGroup', 'selectionColour', 'customColour', 'copiedSelection', 'copiedTerritoriesOnly', 'csvSaved', 'csvFailed'] }
|
||||||
const cat = (l, f) => JSON.parse(readFileSync(P('library/i18n/locales', l, `${f}.json`), 'utf8'))
|
const cat = (l, f) => JSON.parse(readFileSync(P('library/i18n/locales', l, `${f}.json`), 'utf8'))
|
||||||
const bad = []
|
const bad = []
|
||||||
for (const [f, keys] of Object.entries(KEYS)) {
|
for (const [f, keys] of Object.entries(KEYS)) {
|
||||||
|
|
|
||||||
78
internal/check-poster-sizes.mjs
Normal file
78
internal/check-poster-sizes.mjs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* check-poster-sizes.mjs — the poster-size LADDER (library/constants/posterSizes.js) says what it promises.
|
||||||
|
*
|
||||||
|
* Every print surface offers these sizes through one picker, and each renderer maps a key to its own
|
||||||
|
* geometry. So the table itself must be right: a wrong sheet count or overlap prints a poster that does not
|
||||||
|
* glue up to its size, and a landscape size breaks the portrait-only rule.
|
||||||
|
*
|
||||||
|
* ASSERTS: the picker order lists exactly the ladder; no size is both offered and legacy; only 66x66 and 60x96
|
||||||
|
* are glued from several sheets; every key is dot-free (i18n paths, URLs); every size is
|
||||||
|
* PORTRAIT (h ≥ w); the sheets glue up to the finished size (count × sheet − overlaps, in `direction`); a
|
||||||
|
* photo slot is allowed on exactly the large sizes (66x66, 60x96); paper prints through the browser and the
|
||||||
|
* rest through a print-shop PDF; every alias resolves; paperFor() picks Letter or A4 by region.
|
||||||
|
* --self-test feeds broken copies of the table and must see each one refused.
|
||||||
|
*/
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const S = await import(pathToFileURL(join(ROOT, 'library/constants/posterSizes.js')).href)
|
||||||
|
|
||||||
|
const PHOTO_SIZES = ['66x66', '60x96']
|
||||||
|
// Every size up to 36x66 prints on ONE sheet; only these two are glued from several.
|
||||||
|
const MULTI_SHEET = ['66x66', '60x96']
|
||||||
|
function audit({ ladder, order, legacy, aliases }) {
|
||||||
|
const bad = []
|
||||||
|
const keys = Object.keys(ladder)
|
||||||
|
if (order.join() !== keys.join()) bad.push(`the picker order [${order}] is not the ladder [${keys}]`)
|
||||||
|
for (const [k, e] of [...Object.entries(ladder), ...Object.entries(legacy)]) {
|
||||||
|
if (!/^[a-z0-9]+$/.test(k)) bad.push(`${k}: a key must be dot-free lowercase letters/digits`)
|
||||||
|
if (e.id !== k) bad.push(`${k}: its id is "${e.id}"`)
|
||||||
|
const { w, h } = e.inches
|
||||||
|
if (!(h >= w)) bad.push(`${k}: ${w}×${h} in is landscape; every poster is portrait`)
|
||||||
|
const s = e.sheets
|
||||||
|
const W = s.direction === 'horizontal' ? s.count * s.sheetW - (s.count - 1) * s.joinIn : s.sheetW
|
||||||
|
const H = s.direction === 'vertical' ? s.count * s.sheetH - (s.count - 1) * s.joinIn : s.sheetH
|
||||||
|
if (W !== w || H !== h) bad.push(`${k}: ${s.count} sheet(s) of ${s.sheetW}×${s.sheetH} (${s.direction}, ${s.joinIn} in overlap) glue up to ${W}×${H}, not ${w}×${h}`)
|
||||||
|
if (s.count === 1 && s.joinIn !== 0) bad.push(`${k}: one sheet cannot have a ${s.joinIn} in overlap`)
|
||||||
|
}
|
||||||
|
for (const k of Object.keys(legacy)) if (ladder[k]) bad.push(`${k} is both offered and legacy`)
|
||||||
|
const multi = keys.filter((k) => ladder[k].sheets.count > 1)
|
||||||
|
if (multi.join() !== MULTI_SHEET.join()) bad.push(`glued sizes are [${multi}], expected [${MULTI_SHEET}]; every other size is one sheet`)
|
||||||
|
const photos = keys.filter((k) => ladder[k].photos)
|
||||||
|
if (photos.join() !== PHOTO_SIZES.join()) bad.push(`photo slots are allowed on [${photos}], expected [${PHOTO_SIZES}] (the large sizes)`)
|
||||||
|
for (const k of keys) {
|
||||||
|
const want = ladder[k].paper ? 'print' : 'pdf'
|
||||||
|
if (ladder[k].path !== want) bad.push(`${k}: path "${ladder[k].path}", expected "${want}"`)
|
||||||
|
}
|
||||||
|
for (const [a, t] of Object.entries(aliases)) if (!ladder[t] && !legacy[t]) bad.push(`alias ${a} → ${t}, which is not a size`)
|
||||||
|
return bad
|
||||||
|
}
|
||||||
|
const table = { ladder: S.POSTER_LADDER, order: S.POSTER_LADDER_ORDER, legacy: S.POSTER_LADDER_LEGACY, aliases: S.POSTER_LADDER_ALIASES }
|
||||||
|
const problems = audit(table)
|
||||||
|
const paper = [['en-US', 'letter'], ['es-MX', 'letter'], ['fr-CA', 'letter'], ['en-GB', 'a4'], ['ar', 'a4'], ['zh-CN', 'a4'], ['en', 'letter']]
|
||||||
|
for (const [tag, want] of paper) if (S.paperFor(tag) !== want) problems.push(`paperFor('${tag}') is ${S.paperFor(tag)}, expected ${want}`)
|
||||||
|
if (S.resolvePosterSize('5x8ft')?.id !== '60x96' || S.resolvePosterSize('nope') !== null) problems.push('resolvePosterSize does not resolve an alias, or invents an unknown size')
|
||||||
|
|
||||||
|
if (process.argv.includes('--self-test')) {
|
||||||
|
if (problems.length) { console.error('POSTER-SIZES SELF-TEST: FAIL — the real table already fails; fix that first.'); process.exit(1) }
|
||||||
|
const clone = () => JSON.parse(JSON.stringify(table))
|
||||||
|
const poisons = [
|
||||||
|
['one sheet short', (t) => { t.ladder['60x96'].sheets.count = 2 }],
|
||||||
|
['no overlap', (t) => { t.ladder['66x66'].sheets.joinIn = 0 }],
|
||||||
|
['landscape', (t) => { t.ladder['36x60'].inches = { w: 60, h: 36 }; t.ladder['36x60'].sheets = { sheetW: 60, sheetH: 36, count: 1, direction: 'vertical', joinIn: 0 } }],
|
||||||
|
['photos on a small size', (t) => { t.ladder['36x60'].photos = true }],
|
||||||
|
['a dotted key', (t) => { t.ladder['5.5x5.5'] = { ...t.ladder['66x66'], id: '5.5x5.5' }; t.order.push('5.5x5.5') }],
|
||||||
|
['order drift', (t) => { t.order.reverse() }],
|
||||||
|
['a dangling alias', (t) => { t.aliases['4x6ft'] = '48x72' }],
|
||||||
|
['paper through a PDF', (t) => { t.ladder.paper.path = 'pdf' }],
|
||||||
|
['offered and legacy at once', (t) => { t.legacy['36x48'] = t.ladder['36x48'] }],
|
||||||
|
['a single-sheet size split in two', (t) => { t.ladder['36x66'].sheets = { sheetW: 36, sheetH: 36, count: 2, direction: 'vertical', joinIn: 6 } }],
|
||||||
|
]
|
||||||
|
let missed = 0
|
||||||
|
for (const [name, poison] of poisons) { const t = clone(); poison(t); const caught = audit(t).length > 0; console.log(` ${name}: ${caught ? 'caught' : 'NOT CAUGHT'}`); if (!caught) missed++ }
|
||||||
|
if (missed) { console.error(`POSTER-SIZES SELF-TEST: FAIL — ${missed} poison(s) not caught.`); process.exit(1) }
|
||||||
|
console.log(`POSTER-SIZES SELF-TEST: PASS — all ${poisons.length} broken tables refused.`); process.exit(0)
|
||||||
|
}
|
||||||
|
if (problems.length) { console.error('POSTER-SIZES: FAIL\n' + problems.map((p) => ' ✗ ' + p).join('\n')); process.exit(1) }
|
||||||
|
console.log(`POSTER-SIZES: PASS — ${S.POSTER_LADDER_ORDER.length} sizes (${S.POSTER_LADDER_ORDER.join(' · ')}), portrait, sheets glue up, photos on ${PHOTO_SIZES.join(' + ')}, ${Object.keys(S.POSTER_LADDER_ALIASES).length} aliases resolve, paper by region.`)
|
||||||
93
internal/check-upg-count.mjs
Normal file
93
internal/check-upg-count.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// check-upg-count — UPG-100 never writes its list's TOTAL into a sentence. The wall's intro printed "2,106" in all
|
||||||
|
// eleven languages after the list had grown to 2,107, because the number lived inside the translated text. Now every
|
||||||
|
// sentence takes it as {n}, from the funnel's last step (plugin-designs/my-upg-100-list/data/upg-total.js), formatted
|
||||||
|
// for the reader's locale. Headless. Asserts:
|
||||||
|
// A. the total is known (vision-funnel.json's last step) and upgTotal() reads it;
|
||||||
|
// B. no upgMap value in any of the 11 locales contains the total (or the stale 2,106) in ANY digit system: Latin
|
||||||
|
// with any separator, Arabic-Indic, Devanagari. A built-in NEGATIVE CONTROL plants "2,106" and must be caught;
|
||||||
|
// C. upgMap.wallIntroA carries {n} in all 11 locales;
|
||||||
|
// D. the consumers pass the live total: poster-wall.vue reads upgMap.wallIntroA with n (not the frozen
|
||||||
|
// messages.upg100WallIntroA), and upg-100-list.vue passes no number literal to fetchingUupgs;
|
||||||
|
// E. ONE SENTENCE, ONE DIGIT SYSTEM: the wall intro, rendered through each locale's real catalogue, writes its
|
||||||
|
// numbers in one digit system (ar: Arabic-Indic, so ٢٬١٠٧ beside ٢٧ and ٩:١٤). A NEGATIVE CONTROL renders ar the
|
||||||
|
// old way (plain Intl 'ar', which CLDR formats Western) and must be flagged as mixed.
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { resolve, dirname, join } from 'node:path'
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||||
|
|
||||||
|
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const P = (...a) => join(REPO, ...a)
|
||||||
|
const fails = [], notes = []
|
||||||
|
const ok = (cond, msg, note) => { if (!cond) fails.push(msg); else if (note) notes.push(` ok ${note}`) }
|
||||||
|
const LANGS = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh']
|
||||||
|
|
||||||
|
let upgTotal, formatCount, countInSentence, funnel
|
||||||
|
try {
|
||||||
|
;({ upgTotal, formatCount, countInSentence } = await import(pathToFileURL(P('plugin-designs/my-upg-100-list/data/upg-total.js')).href))
|
||||||
|
funnel = JSON.parse(readFileSync(P('data/people-groups/vision-funnel.json'), 'utf8'))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`UPG-COUNT: NOT MEASURED (exit 75) — an input did not load: ${e.message}`)
|
||||||
|
process.exit(75)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── A ──
|
||||||
|
const total = upgTotal(funnel)
|
||||||
|
ok(total > 1000, `the list's total reads as ${total} from vision-funnel.json`, `the list's total is ${total}, from the funnel`)
|
||||||
|
|
||||||
|
// ── B: a digit-system-blind scan ──
|
||||||
|
const NATIVE = { '٠': 0, '١': 1, '٢': 2, '٣': 3, '٤': 4, '٥': 5, '٦': 6, '٧': 7, '٨': 8, '٩': 9, '०': 0, '१': 1, '२': 2, '३': 3, '४': 4, '५': 5, '६': 6, '७': 7, '८': 8, '९': 9 }
|
||||||
|
const numbersIn = (s) => String(s).replace(/[٠-٩०-९]/g, (d) => NATIVE[d])
|
||||||
|
.replace(/(\d)[,.'\u0020\u00A0\u202F\u2009\u066C](?=\d{3}\b)/g, '$1') // drop thousands separators (fr U+202F, ru U+00A0, ar U+066C)
|
||||||
|
.match(/\d+/g) || []
|
||||||
|
const FORBIDDEN = new Set([String(total), '2106'])
|
||||||
|
const hasTotal = (s) => numbersIn(s).some((n) => FORBIDDEN.has(n))
|
||||||
|
ok(hasTotal('We did the same with the 2,106 unreached') && hasTotal('فعلنا المثل مع ٢١٠٦ شعوب') && hasTotal(`mit den ${String(total).replace(/(\d)(\d{3})$/, '$1.$2')} Gruppen`)
|
||||||
|
&& hasTotal(`les ${new Intl.NumberFormat('fr').format(total)} groupes`) && hasTotal(`из ${new Intl.NumberFormat('ru').format(total)} групп`),
|
||||||
|
'NEGATIVE CONTROL: the scanner missed a planted total, so it cannot guard anything', 'negative control: a planted 2,106 / ٢١٠٦ / 2.107 / fr 2 107 / ru 2 107 is caught')
|
||||||
|
const leaks = []
|
||||||
|
for (const l of LANGS) {
|
||||||
|
const cat = JSON.parse(readFileSync(P('library/i18n/locales', l, 'upgMap.json'), 'utf8'))
|
||||||
|
for (const [k, v] of Object.entries(cat)) if (typeof v === 'string' && hasTotal(v)) leaks.push(`${l} upgMap.${k}`)
|
||||||
|
}
|
||||||
|
ok(!leaks.length, `the total is written into a sentence: ${leaks.slice(0, 6).join(', ')}`, `no upgMap sentence in the 11 locales carries the total (${total}) or the stale 2,106`)
|
||||||
|
|
||||||
|
// ── C ──
|
||||||
|
const noN = LANGS.filter((l) => !String(JSON.parse(readFileSync(P('library/i18n/locales', l, 'upgMap.json'), 'utf8')).wallIntroA || '').includes('{n}'))
|
||||||
|
ok(!noN.length, `upgMap.wallIntroA has no {n} in: ${noN.join(', ')}`, 'the wall intro takes the total as {n} in all 11 locales')
|
||||||
|
|
||||||
|
// ── D ──
|
||||||
|
const wall = readFileSync(P('plugin-designs/my-upg-100-list/poster-wall.vue'), 'utf8')
|
||||||
|
const map = readFileSync(P('plugin-designs/my-upg-100-list/upg-100-list.vue'), 'utf8')
|
||||||
|
ok(/countInSentence\(t, 'upgMap\.wallIntroA', upgTotal\(/.test(wall) && !/t\('messages\.upg100WallIntroA'\)/.test(wall),
|
||||||
|
'poster-wall.vue does not render the intro from upgMap.wallIntroA with the live total', 'the wall intro renders the live total')
|
||||||
|
ok(!/fetchingUupgs', \{ n: \d/.test(map) && /countInSentence\(t, 'messages\.fetchingUupgs', upgTotal\(/.test(map),
|
||||||
|
'upg-100-list.vue passes a number literal to fetchingUupgs', 'the fetching status shows the live total')
|
||||||
|
|
||||||
|
// ── E: one sentence, one digit system ──
|
||||||
|
const SYSTEMS = [['latn', /[0-9]/], ['arab', /[\u0660-\u0669]/], ['arabext', /[\u06F0-\u06F9]/], ['deva', /[\u0966-\u096F]/], ['beng', /[\u09E6-\u09EF]/]]
|
||||||
|
const systemsIn = (s) => SYSTEMS.filter(([, re]) => re.test(String(s))).map(([nu]) => nu)
|
||||||
|
const trFor = (l) => {
|
||||||
|
const c = JSON.parse(readFileSync(P('library/i18n/locales', l, 'upgMap.json'), 'utf8'))
|
||||||
|
return (k, p = {}) => String(c[k.replace(/^upgMap\./, '')] ?? k).replace(/\{(\w+)\}/g, (m, x) => (p[x] != null ? String(p[x]) : m))
|
||||||
|
}
|
||||||
|
const arOld = trFor('ar')('upgMap.wallIntroA', { n: formatCount(total, 'ar') })
|
||||||
|
ok(systemsIn(arOld).length > 1, `NEGATIVE CONTROL: the old ar render reads as ONE digit system (${systemsIn(arOld).join('+')}), so this check cannot see the defect`,
|
||||||
|
`negative control: the old ar render (plain Intl 'ar' = "${formatCount(total, 'ar')}") is flagged as mixed (${systemsIn(arOld).join('+')})`)
|
||||||
|
const mixed = []
|
||||||
|
for (const l of LANGS) {
|
||||||
|
const s = countInSentence(trFor(l), 'upgMap.wallIntroA', total, l)
|
||||||
|
if (systemsIn(s).length > 1) mixed.push(`${l} ${systemsIn(s).join('+')}`)
|
||||||
|
if (!numbersIn(s).includes(String(total))) mixed.push(`${l} does not show ${total}`)
|
||||||
|
}
|
||||||
|
const arNew = countInSentence(trFor('ar'), 'upgMap.wallIntroA', total, 'ar')
|
||||||
|
ok(!mixed.length, `the wall intro: ${mixed.join('; ')}`,
|
||||||
|
`the wall intro shows ${total} in ONE digit system in all 11 locales (ar: "${(arNew.match(/[\u0660-\u0669٬]{3,}/) || ['?'])[0]}")`)
|
||||||
|
|
||||||
|
for (const n of notes) console.log(n)
|
||||||
|
if (fails.length) {
|
||||||
|
console.error('UPG-COUNT: FAIL')
|
||||||
|
for (const f of fails) console.error(` ✗ ${f}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
console.log(`UPG-COUNT: PASS — the list's total (${total}) lives in one place and every sentence takes it as {n}.`)
|
||||||
|
|
@ -96,3 +96,65 @@ export function listPosterSizes() {
|
||||||
export function listByCategory(category) {
|
export function listByCategory(category) {
|
||||||
return listPosterSizes().filter((s) => s.category === category);
|
return listPosterSizes().filter((s) => s.category === category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── THE POSTER-SIZE LADDER (every print surface, 2026-09-30) ────────────────────────────────────
|
||||||
|
// ONE table of the sizes a READER can pick for a poster, shared by every print surface (the poster
|
||||||
|
// walls, the map's export, Bless, the map-print template) through ONE print picker. It is separate from
|
||||||
|
// POSTER_SIZES above (the map exporter's US/ISO list), and written as plain literals, so a bundle that
|
||||||
|
// never imports it carries none of it.
|
||||||
|
//
|
||||||
|
// KEYS are WxH in INCHES (this file's existing convention: '24x36', '27x40'); they are dot-free, so
|
||||||
|
// they are safe in i18n key paths, URLs and stored choices. PORTRAIT ONLY: h ≥ w for every size (66x66
|
||||||
|
// is square). `sheets` is what a printer actually feeds, named exactly as panelSheets.js's planPanels()
|
||||||
|
// parameters, so a consumer passes it straight in: `count` sheets of sheetW × sheetH, stacked in
|
||||||
|
// `direction`, each overlapping the next by `joinIn` (glued), which gives the finished w × h.
|
||||||
|
// `photos` means a photo slot beside each name is ALLOWED at this size (there is room); which photos
|
||||||
|
// print is the renderer's call. `path` is how the size reaches paper: 'print' = the browser's own print
|
||||||
|
// (any home printer; a phone prints Letter/A4 only), 'pdf' = a print-shop PDF, one page per sheet at
|
||||||
|
// exact inches, since no phone and few home printers take 3-8 ft paper.
|
||||||
|
export const POSTER_LADDER = {
|
||||||
|
paper: { id: 'paper', inches: { w: 8.5, h: 11 }, paper: true,
|
||||||
|
sheets: { sheetW: 8.5, sheetH: 11, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'print' },
|
||||||
|
'18x24': { id: '18x24', inches: { w: 18, h: 24 },
|
||||||
|
sheets: { sheetW: 18, sheetH: 24, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||||
|
'36x36': { id: '36x36', inches: { w: 36, h: 36 },
|
||||||
|
sheets: { sheetW: 36, sheetH: 36, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||||
|
'36x48': { id: '36x48', inches: { w: 36, h: 48 },
|
||||||
|
sheets: { sheetW: 36, sheetH: 48, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||||
|
'36x60': { id: '36x60', inches: { w: 36, h: 60 },
|
||||||
|
sheets: { sheetW: 36, sheetH: 60, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||||
|
'36x66': { id: '36x66', inches: { w: 36, h: 66 },
|
||||||
|
sheets: { sheetW: 36, sheetH: 66, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||||
|
'66x66': { id: '66x66', inches: { w: 66, h: 66 },
|
||||||
|
sheets: { sheetW: 36, sheetH: 66, count: 2, direction: 'horizontal', joinIn: 6 }, photos: true, path: 'pdf' },
|
||||||
|
'60x96': { id: '60x96', inches: { w: 60, h: 96 },
|
||||||
|
sheets: { sheetW: 60, sheetH: 36, count: 3, direction: 'vertical', joinIn: 6 }, photos: true, path: 'pdf' },
|
||||||
|
}
|
||||||
|
/** The picker's order, smallest first. Every size up to 36x66 is one sheet; 66x66 and 60x96 are glued. */
|
||||||
|
export const POSTER_LADDER_ORDER = ['paper', '18x24', '36x36', '36x48', '36x60', '36x66', '66x66', '60x96']
|
||||||
|
/** A size that still resolves (stored choices, today's defaults) but is NOT offered in the picker. */
|
||||||
|
export const POSTER_LADDER_LEGACY = {
|
||||||
|
'24x36': { id: '24x36', inches: { w: 24, h: 36 },
|
||||||
|
sheets: { sheetW: 24, sheetH: 36, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||||
|
}
|
||||||
|
/** Older names for the same sizes (poster-render's FORMATS, Bless's PRESETS, the map exporter). */
|
||||||
|
export const POSTER_LADDER_ALIASES = {
|
||||||
|
'3x3': '36x36', '3x3ft': '36x36', '3x5': '36x60', '3x5ft': '36x60',
|
||||||
|
'3x4': '36x48', '3x4ft': '36x48', '5x8': '60x96', '5x8ft': '60x96',
|
||||||
|
'2x3': '24x36', '2x3ft': '24x36', letter: 'paper', a4: 'paper',
|
||||||
|
}
|
||||||
|
/** A key, an alias or a legacy key → its ladder entry; null when unknown. */
|
||||||
|
export function resolvePosterSize(key) {
|
||||||
|
const k = POSTER_LADDER_ALIASES[key] || key
|
||||||
|
return POSTER_LADDER[k] || POSTER_LADDER_LEGACY[k] || null
|
||||||
|
}
|
||||||
|
// Countries that print on US Letter; everywhere else "Standard printer paper" means A4. Keyed by the
|
||||||
|
// REGION of a locale tag (en-US, es-MX); a tag without a region falls back to its language's usual paper.
|
||||||
|
const LETTER_REGIONS = new Set(['US', 'CA', 'MX', 'PH', 'CL', 'CO', 'VE', 'CR', 'GT', 'PA', 'DO', 'PR', 'SV', 'NI', 'HN', 'BO'])
|
||||||
|
/** 'letter' or 'a4' for a locale tag (e.g. navigator.language). */
|
||||||
|
export function paperFor(localeTag) {
|
||||||
|
const m = String(localeTag || '').match(/^([a-z]{2,3})(?:[-_]([A-Za-z]{2}))?/i)
|
||||||
|
if (!m) return 'letter'
|
||||||
|
if (m[2]) return LETTER_REGIONS.has(m[2].toUpperCase()) ? 'letter' : 'a4'
|
||||||
|
return m[1].toLowerCase() === 'en' ? 'letter' : 'a4'
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "تصدير CSV",
|
"exportCsv": "تصدير CSV",
|
||||||
"copyNotRegroupable": "لا يمكن إعادة تجميعها معًا: انسخ ولايات أو مقاطعات بلد واحد، أو البلدان، كلًّا على حدة",
|
"copyNotRegroupable": "لا يمكن إعادة تجميعها معًا: انسخ ولايات أو مقاطعات بلد واحد، أو البلدان، كلًّا على حدة",
|
||||||
"displayName": "ملصقات قائمة UPG 100",
|
"displayName": "ملصقات قائمة UPG 100",
|
||||||
"wallHeading": "ملصقات قائمة UPG 100"
|
"wallHeading": "ملصقات قائمة UPG 100",
|
||||||
|
"wallIntroA": "كيف تُطعم جمعًا من خمسة آلاف؟ أجلسهم يسوع جماعاتٍ من خمسين ومن مئة، فصار المستحيل قابلًا للمشاركة (لوقا ٩:١٤). فعلنا المثل مع {n} شعوب غير مبلَّغة: ٢٧ قائمة من نحو خمسين إلى مئة. تُطبع كل قائمة حزمةَ صلاة وملصقًا، ليحمل ٢٧ تلميذًا أو كنيسة أو قائدًا محليًا كلٌّ واحدة — مصلّين لأجلهم ",
|
||||||
|
"peopleStillLoading": "لا تزال الشعوب قيد التحميل، حاول مرة أخرى بعد لحظة"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "CSV exportieren",
|
"exportCsv": "CSV exportieren",
|
||||||
"copyNotRegroupable": "Nicht gemeinsam umgruppierbar: Bundesstaaten oder Provinzen eines Landes oder die Länder getrennt kopieren",
|
"copyNotRegroupable": "Nicht gemeinsam umgruppierbar: Bundesstaaten oder Provinzen eines Landes oder die Länder getrennt kopieren",
|
||||||
"displayName": "Poster der UPG 100 Liste",
|
"displayName": "Poster der UPG 100 Liste",
|
||||||
"wallHeading": "Poster der UPG 100 Liste"
|
"wallHeading": "Poster der UPG 100 Liste",
|
||||||
|
"wallIntroA": "Wie würden Sie eine Menge von fünftausend speisen? Jesus ließ sie in Gruppen zu fünfzig und hundert lagern, und das Unmögliche wurde teilbar (Lukas 9,14). Dasselbe haben wir mit den {n} unerreichten Volksgruppen getan: 27 Listen von etwa fünfzig bis hundert. Jede Liste druckt als Gebetspaket und als Poster, damit 27 Jünger, Gemeinden oder einheimische Leiter je eine tragen können — und für sie beten ",
|
||||||
|
"peopleStillLoading": "die Volksgruppen werden noch geladen; versuchen Sie es gleich noch einmal"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "Export CSV",
|
"exportCsv": "Export CSV",
|
||||||
"copyNotRegroupable": "Can't be regrouped together: copy one country's states or provinces, or the countries, separately",
|
"copyNotRegroupable": "Can't be regrouped together: copy one country's states or provinces, or the countries, separately",
|
||||||
"displayName": "UPG 100 List Posters",
|
"displayName": "UPG 100 List Posters",
|
||||||
"wallHeading": "UPG 100 List Posters"
|
"wallHeading": "UPG 100 List Posters",
|
||||||
|
"wallIntroA": "How would you feed a crowd of five thousand? Jesus sat them down in groups of fifty and a hundred, and the impossible became shareable (Luke 9:14). We did the same with the {n} unreached people groups: 27 lists of about fifty to a hundred. Each list prints as a prayer packet and a poster, so 27 disciples, churches or local leaders can each carry one — praying for them ",
|
||||||
|
"peopleStillLoading": "the people groups are still loading, try again in a moment"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "Exportar CSV",
|
"exportCsv": "Exportar CSV",
|
||||||
"copyNotRegroupable": "No se pueden reagrupar juntos: copia por separado los estados o provincias de un solo país, o los países",
|
"copyNotRegroupable": "No se pueden reagrupar juntos: copia por separado los estados o provincias de un solo país, o los países",
|
||||||
"displayName": "Carteles de la lista UPG 100",
|
"displayName": "Carteles de la lista UPG 100",
|
||||||
"wallHeading": "Carteles de la lista UPG 100"
|
"wallHeading": "Carteles de la lista UPG 100",
|
||||||
|
"wallIntroA": "¿Cómo alimentarías a una multitud de cinco mil? Jesús los sentó en grupos de cincuenta y de cien, y lo imposible se volvió compartible (Lucas 9:14). Hicimos lo mismo con los {n} pueblos no alcanzados: 27 listas de unos cincuenta a cien. Cada lista se imprime como paquete de oración y cartel, para que 27 discípulos, iglesias o líderes locales lleven una cada uno — orando por ellos ",
|
||||||
|
"peopleStillLoading": "los pueblos aún se están cargando; inténtalo de nuevo en un momento"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "Exporter en CSV",
|
"exportCsv": "Exporter en CSV",
|
||||||
"copyNotRegroupable": "Impossible de les regrouper ensemble : copiez séparément les États ou provinces d’un seul pays, ou les pays",
|
"copyNotRegroupable": "Impossible de les regrouper ensemble : copiez séparément les États ou provinces d’un seul pays, ou les pays",
|
||||||
"displayName": "Affiches de la liste UPG 100",
|
"displayName": "Affiches de la liste UPG 100",
|
||||||
"wallHeading": "Affiches de la liste UPG 100"
|
"wallHeading": "Affiches de la liste UPG 100",
|
||||||
|
"wallIntroA": "Comment nourririez-vous une foule de cinq mille personnes ? Jésus les a fait asseoir par groupes de cinquante et de cent, et l’impossible est devenu partageable (Luc 9.14). Nous avons fait de même avec les {n} peuples non atteints : 27 listes d’environ cinquante à cent. Chaque liste s’imprime en dossier de prière et en affiche, pour que 27 disciples, Églises ou responsables locaux en portent chacun une — en priant pour eux ",
|
||||||
|
"peopleStillLoading": "les peuples sont encore en cours de chargement ; réessayez dans un instant"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "CSV निर्यात करें",
|
"exportCsv": "CSV निर्यात करें",
|
||||||
"copyNotRegroupable": "इन्हें एक साथ पुनर्समूहित नहीं किया जा सकता: एक देश के राज्य या प्रांत, या देश, अलग-अलग कॉपी करें",
|
"copyNotRegroupable": "इन्हें एक साथ पुनर्समूहित नहीं किया जा सकता: एक देश के राज्य या प्रांत, या देश, अलग-अलग कॉपी करें",
|
||||||
"displayName": "UPG 100 सूची पोस्टर",
|
"displayName": "UPG 100 सूची पोस्टर",
|
||||||
"wallHeading": "UPG 100 सूची पोस्टर"
|
"wallHeading": "UPG 100 सूची पोस्टर",
|
||||||
|
"wallIntroA": "पाँच हज़ार की भीड़ को आप कैसे खिलाते? यीशु ने उन्हें पचास-पचास और सौ-सौ के समूहों में बिठाया, और असंभव बाँटने योग्य हो गया (लूका 9:14). हमने {n} अनपहुँचे जनसमूहों के साथ वही किया: लगभग पचास से सौ तक की 27 सूचियाँ. हर सूची प्रार्थना पैकेट और पोस्टर के रूप में छपती है, ताकि 27 शिष्य, कलीसियाएँ या स्थानीय अगुवे एक-एक ले सकें — उनके लिए प्रार्थना करते हुए ",
|
||||||
|
"peopleStillLoading": "जनसमूह अभी लोड हो रहे हैं, थोड़ी देर में फिर कोशिश करें"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "Esporta CSV",
|
"exportCsv": "Esporta CSV",
|
||||||
"copyNotRegroupable": "Non si possono raggruppare insieme: copia separatamente gli stati o le province di un solo paese, o i paesi",
|
"copyNotRegroupable": "Non si possono raggruppare insieme: copia separatamente gli stati o le province di un solo paese, o i paesi",
|
||||||
"displayName": "Poster della lista UPG 100",
|
"displayName": "Poster della lista UPG 100",
|
||||||
"wallHeading": "Poster della lista UPG 100"
|
"wallHeading": "Poster della lista UPG 100",
|
||||||
|
"wallIntroA": "Come sfameresti una folla di cinquemila? Gesù li fece sedere a gruppi di cinquanta e di cento, e l’impossibile divenne condivisibile (Luca 9:14). Abbiamo fatto lo stesso con i {n} popoli non raggiunti: 27 liste di circa cinquanta-cento. Ogni lista si stampa come pacchetto di preghiera e manifesto, così 27 discepoli, chiese o responsabili locali ne portano una ciascuno — pregando per loro ",
|
||||||
|
"peopleStillLoading": "i popoli si stanno ancora caricando; riprova tra un momento"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "Exportar CSV",
|
"exportCsv": "Exportar CSV",
|
||||||
"copyNotRegroupable": "Não podem ser reagrupados juntos: copie separadamente os estados ou províncias de um só país, ou os países",
|
"copyNotRegroupable": "Não podem ser reagrupados juntos: copie separadamente os estados ou províncias de um só país, ou os países",
|
||||||
"displayName": "Cartazes da lista UPG 100",
|
"displayName": "Cartazes da lista UPG 100",
|
||||||
"wallHeading": "Cartazes da lista UPG 100"
|
"wallHeading": "Cartazes da lista UPG 100",
|
||||||
|
"wallIntroA": "Como você alimentaria uma multidão de cinco mil? Jesus os sentou em grupos de cinquenta e de cem, e o impossível tornou-se compartilhável (Lucas 9.14). Fizemos o mesmo com os {n} povos não alcançados: 27 listas de cerca de cinquenta a cem. Cada lista imprime como pacote de oração e cartaz, para que 27 discípulos, igrejas ou líderes locais levem uma cada um — orando por eles ",
|
||||||
|
"peopleStillLoading": "os povos ainda estão carregando; tente de novo em um instante"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "Exportă CSV",
|
"exportCsv": "Exportă CSV",
|
||||||
"copyNotRegroupable": "Nu pot fi regrupate împreună: copiază separat statele sau provinciile unei singure țări, sau țările",
|
"copyNotRegroupable": "Nu pot fi regrupate împreună: copiază separat statele sau provinciile unei singure țări, sau țările",
|
||||||
"displayName": "Afișe din lista UPG 100",
|
"displayName": "Afișe din lista UPG 100",
|
||||||
"wallHeading": "Afișe din lista UPG 100"
|
"wallHeading": "Afișe din lista UPG 100",
|
||||||
|
"wallIntroA": "Cum ai hrăni o mulțime de cinci mii? Isus i-a așezat în grupuri de cincizeci și de o sută, iar imposibilul a devenit împărțibil (Luca 9:14). Am făcut la fel cu cele {n} popoare neatinse: 27 de liste de aproximativ cincizeci până la o sută. Fiecare listă se tipărește ca pachet de rugăciune și afiș, ca 27 de ucenici, biserici sau lideri locali să poarte fiecare câte una — rugându-se pentru ei ",
|
||||||
|
"peopleStillLoading": "popoarele încă se încarcă; încearcă din nou peste o clipă"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "Экспорт в CSV",
|
"exportCsv": "Экспорт в CSV",
|
||||||
"copyNotRegroupable": "Нельзя перегруппировать вместе: копируйте отдельно штаты или провинции одной страны либо страны",
|
"copyNotRegroupable": "Нельзя перегруппировать вместе: копируйте отдельно штаты или провинции одной страны либо страны",
|
||||||
"displayName": "Плакаты списка UPG 100",
|
"displayName": "Плакаты списка UPG 100",
|
||||||
"wallHeading": "Плакаты списка UPG 100"
|
"wallHeading": "Плакаты списка UPG 100",
|
||||||
|
"wallIntroA": "Как накормить толпу в пять тысяч человек? Иисус рассадил их группами по пятьдесят и по сто, и невозможное стало посильным (Лк. 9:14). Мы поступили так же с {n} неохваченными народами: 27 списков примерно по пятьдесят-сто. Каждый список печатается как молитвенный пакет и плакат, чтобы 27 учеников, церквей или местных служителей взяли по одному — молясь о них ",
|
||||||
|
"peopleStillLoading": "народы ещё загружаются; попробуйте ещё раз через мгновение"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@
|
||||||
"exportCsv": "导出 CSV",
|
"exportCsv": "导出 CSV",
|
||||||
"copyNotRegroupable": "无法一起重新分组:请分别复制同一国家的邦或省,或各个国家",
|
"copyNotRegroupable": "无法一起重新分组:请分别复制同一国家的邦或省,或各个国家",
|
||||||
"displayName": "UPG 100 名单海报",
|
"displayName": "UPG 100 名单海报",
|
||||||
"wallHeading": "UPG 100 名单海报"
|
"wallHeading": "UPG 100 名单海报",
|
||||||
|
"wallIntroA": "你会怎样喂饱五千人的群众?耶稣让他们一排五十、一排一百地坐下,不可能的事就变得可以分担了(路加福音 9:14)。我们对 {n} 个未得之民做了同样的事:27 份名单,每份约五十到一百。每份名单都能印成祷告手册和海报,让 27 位门徒、教会或当地领袖各领一份 — 为他们祷告 ",
|
||||||
|
"peopleStillLoading": "族群仍在加载中,请稍后再试"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@
|
||||||
"check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison",
|
"check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison",
|
||||||
"check:csv-shape": "node internal/check-csv-export-shape.mjs",
|
"check:csv-shape": "node internal/check-csv-export-shape.mjs",
|
||||||
"check:csv-shape:self-test": "node internal/check-csv-export-shape.mjs --self-test",
|
"check:csv-shape:self-test": "node internal/check-csv-export-shape.mjs --self-test",
|
||||||
|
"check:poster-sizes:self-test": "node internal/check-poster-sizes.mjs --self-test",
|
||||||
|
"check:poster-sizes": "node internal/check-poster-sizes.mjs",
|
||||||
"check:pin-hint-floor": "node internal/check-pin-hint-floor.mjs",
|
"check:pin-hint-floor": "node internal/check-pin-hint-floor.mjs",
|
||||||
"check:pin-hint-floor:self-test": "node internal/check-pin-hint-floor.mjs --self-test",
|
"check:pin-hint-floor:self-test": "node internal/check-pin-hint-floor.mjs --self-test",
|
||||||
"check:no-fleet-names": "node internal/check-no-fleet-names.mjs",
|
"check:no-fleet-names": "node internal/check-no-fleet-names.mjs",
|
||||||
|
|
@ -226,6 +228,7 @@
|
||||||
"check:packet-a1-structure": "node internal/check-packet-a1-structure.mjs",
|
"check:packet-a1-structure": "node internal/check-packet-a1-structure.mjs",
|
||||||
"check:india-states": "node internal/check-india-states.mjs",
|
"check:india-states": "node internal/check-india-states.mjs",
|
||||||
"check:batch-selection-export": "node internal/check-batch-selection-export.mjs",
|
"check:batch-selection-export": "node internal/check-batch-selection-export.mjs",
|
||||||
|
"check:upg-count": "node internal/check-upg-count.mjs",
|
||||||
"check:zone-title-tier-a": "node internal/check-zone-title-tier-a.mjs",
|
"check:zone-title-tier-a": "node internal/check-zone-title-tier-a.mjs",
|
||||||
"check:evangelical-share": "node internal/check-evangelical-share.mjs",
|
"check:evangelical-share": "node internal/check-evangelical-share.mjs",
|
||||||
"check:map-labels": "node internal/check-map-labels.mjs",
|
"check:map-labels": "node internal/check-map-labels.mjs",
|
||||||
|
|
@ -346,8 +349,8 @@
|
||||||
"dev": "bun x vite",
|
"dev": "bun x vite",
|
||||||
"fix:sri": "node internal/fix-sri.mjs",
|
"fix:sri": "node internal/fix-sri.mjs",
|
||||||
"gallery": "node internal/generate-gallery.mjs",
|
"gallery": "node internal/generate-gallery.mjs",
|
||||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check: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:script-flags 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: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 check:no-fleet-names:self-test check:pin-hint-floor:self-test",
|
"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:script-flags 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: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 check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test",
|
||||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts 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:india-states check:batch-selection-export 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-internal-role-nouns 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:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf 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:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor",
|
"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:india-states check:batch-selection-export 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-internal-role-nouns 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:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf 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:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count",
|
||||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||||
"library-index": "node internal/generate-library-index.mjs",
|
"library-index": "node internal/generate-library-index.mjs",
|
||||||
|
|
@ -456,6 +459,7 @@
|
||||||
"check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.",
|
"check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.",
|
||||||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||||||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||||
|
"check:poster-sizes": "the shared poster-size ladder (library/constants/posterSizes.js) is right: picker order = ladder, dot-free keys, portrait only, the sheets glue up to the finished size, photo slots on exactly 66x66 + 60x96, paper prints and the rest go to a print-shop PDF, every alias resolves, paperFor() picks Letter or A4 by region. Run the `check:poster-sizes:self-test` script for the NEGATIVE test",
|
||||||
"check:pin-hint-floor": "the pin-hint pill sits at the map's bottom on a page that scrolls (the fold is not an obstacle there) and is still lifted above the fold on a full-screen page; the legend sheet and the licence credit still lift it; PinHint.vue takes its floor from pinHintFloor.js. Run the `check:pin-hint-floor:self-test` script for the NEGATIVE test",
|
"check:pin-hint-floor": "the pin-hint pill sits at the map's bottom on a page that scrolls (the fold is not an obstacle there) and is still lifted above the fold on a full-screen page; the legend sheet and the licence credit still lift it; PinHint.vue takes its floor from pinHintFloor.js. Run the `check:pin-hint-floor:self-test` script for the NEGATIVE test",
|
||||||
"check:no-fleet-names": "tracked source (not data/, public/ or the translation catalogues) names no build-team member, attributes no request to a user and carries no dated QA-round tag; an allowance is a named LINE, never a whole file. Run the `check:no-fleet-names:self-test` script for the NEGATIVE test",
|
"check:no-fleet-names": "tracked source (not data/, public/ or the translation catalogues) names no build-team member, attributes no request to a user and carries no dated QA-round tag; an allowance is a named LINE, never a whole file. Run the `check:no-fleet-names:self-test` script for the NEGATIVE test",
|
||||||
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser.",
|
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser.",
|
||||||
|
|
@ -563,6 +567,7 @@
|
||||||
"check:packet-a1-structure": "the prayer packet is built to its specified structure, read page by page from each sheet's CONTENT: full detail = map, triangle, life calendar, % evangelical, profiles, bounded set, centered set, wheel, points; lightweight = map, triangle, profiles, wheel, points. Page 1 places a map-only vertical sheet on the packet's own margin and says it is one of the UPG 100 prayer lists only when a count is supplied; page 2 is one inverted triangle whose packets tier and \"- N\" steps appear only where true; the wheel carries the Metacamp Module 1 PDF's colours and opens at nine o'clock; all 28 legend zones resolve, Deaf included.",
|
"check:packet-a1-structure": "the prayer packet is built to its specified structure, read page by page from each sheet's CONTENT: full detail = map, triangle, life calendar, % evangelical, profiles, bounded set, centered set, wheel, points; lightweight = map, triangle, profiles, wheel, points. Page 1 places a map-only vertical sheet on the packet's own margin and says it is one of the UPG 100 prayer lists only when a count is supplied; page 2 is one inverted triangle whose packets tier and \"- N\" steps appear only where true; the wheel carries the Metacamp Module 1 PDF's colours and opens at nine o'clock; all 28 legend zones resolve, Deaf included.",
|
||||||
"check:india-states": "India's states-and-union-territories table (data/people-groups/india-states.json) stays true to the stamping it is derived from: all 36 ISO 3166-2:IN divisions (28 state + 8 union_territory, Nagaland + Mizoram listed even when empty), every India people group of admin-membership.json in exactly ONE division, every shapeName a feature of the served geometry, NO DRIFT (re-derived from today's inputs it is byte-identical and meta's input sha384s are the files on disk), and pin == table == served copy under a size ceiling. Mutation-tested. Headless; in gates:pre. By the UPG-100 owner.",
|
"check:india-states": "India's states-and-union-territories table (data/people-groups/india-states.json) stays true to the stamping it is derived from: all 36 ISO 3166-2:IN divisions (28 state + 8 union_territory, Nagaland + Mizoram listed even when empty), every India people group of admin-membership.json in exactly ONE division, every shapeName a feature of the served geometry, NO DRIFT (re-derived from today's inputs it is byte-identical and meta's input sha384s are the files on disk), and pin == table == served copy under a size ceiling. Mutation-tested. Headless; in gates:pre. By the UPG-100 owner.",
|
||||||
"check:batch-selection-export": "UPG-100's batch selection EXPORTS its selection (restored 2026-09-30): Copy's first line parses in data/apply-grouping.mjs (names, UUPG total, colour) with the people-group CSV after it; the CSV header, quoting and formula-neutralising hold under a real CSV parse; the HUD wires the colour swatches, hex input, Copy, Export CSV and the records prefetch; the 9 new strings are in all 11 locales, translated. Mutation-tested (CSV first, no formula guard, Copy unwired: each FAILS). Headless; in gates:pre. By the UPG-100 owner.",
|
"check:batch-selection-export": "UPG-100's batch selection EXPORTS its selection (restored 2026-09-30): Copy's first line parses in data/apply-grouping.mjs (names, UUPG total, colour) with the people-group CSV after it; the CSV header, quoting and formula-neutralising hold under a real CSV parse; the HUD wires the colour swatches, hex input, Copy, Export CSV and the records prefetch; the 9 new strings are in all 11 locales, translated. Mutation-tested (CSV first, no formula guard, Copy unwired: each FAILS). Headless; in gates:pre. By the UPG-100 owner.",
|
||||||
|
"check:upg-count": "UPG-100 never writes its list's TOTAL into a sentence (the wall intro printed a stale 2,106 in eleven languages): the total comes from the funnel's last step (data/upg-total.js), no upgMap value in any locale carries it or 2,106 in any digit system (Latin, Arabic-Indic, Devanagari; a built-in negative control plants it), upgMap.wallIntroA takes {n} ×11, and the wall intro + the fetching status pass the live total. Mutation-tested. Headless; in gates:pre. By the UPG-100 owner.",
|
||||||
"check:tree-relative": "the committed tree names no host of its own. With embedBaseUrl empty in the build config, the manifest must say embedBase \".\" and every scriptUrl, iframeUrl and embed snippet src must be relative. check:drop cannot see this: it looks for root-relative \"/...\" references and skips https:// ones, so a tree built with EMBED_BASE_URL (a deploy build) would pass it and pin every snippet to one host, the way the retired CDN route broke. Data addresses (runtime.apiBaseUrl) are not checked. SKIPs when the config sets embedBaseUrl. Static; takes an optional manifest path for poison tests.",
|
"check:tree-relative": "the committed tree names no host of its own. With embedBaseUrl empty in the build config, the manifest must say embedBase \".\" and every scriptUrl, iframeUrl and embed snippet src must be relative. check:drop cannot see this: it looks for root-relative \"/...\" references and skips https:// ones, so a tree built with EMBED_BASE_URL (a deploy build) would pass it and pin every snippet to one host, the way the retired CDN route broke. Data addresses (runtime.apiBaseUrl) are not checked. SKIPs when the config sets embedBaseUrl. Static; takes an optional manifest path for poison tests.",
|
||||||
"check:universal-props": "every plugin reads the props the Plugin Studio header sets for all of them through the library: map engine, locale, Mapbox token and token URL, and the people-group list. It reads each bundle's source AND its shipped bundle and fails on a bypass (a mapbox:// style literal, a private token global, a list fetched around the library); a by-design exception is ALLOWed by name with its reason. --report also prints the matrix. Headless.",
|
"check:universal-props": "every plugin reads the props the Plugin Studio header sets for all of them through the library: map engine, locale, Mapbox token and token URL, and the people-group list. It reads each bundle's source AND its shipped bundle and fails on a bypass (a mapbox:// style literal, a private token global, a list fetched around the library); a by-design exception is ALLOWed by name with its reason. --report also prints the matrix. Headless.",
|
||||||
"check:zone-title-tier-a": "generated zone titles ship for ONE-country zones only: the 3 such zones produce a localized, non-code title in en/es/ar that differs across languages, AND every other zone declines so its hand-written label survives unchanged (a multi-country zone has a name that is not a list of its members). Two-sided, and NOT MEASURED when the country-name supplier cannot resolve a known code.",
|
"check:zone-title-tier-a": "generated zone titles ship for ONE-country zones only: the 3 such zones produce a localized, non-code title in en/es/ar that differs across languages, AND every other zone declines so its hand-written label survives unchanged (a multi-country zone has a name that is not a list of its members). Two-sided, and NOT MEASURED when the country-name supplier cannot resolve a known code.",
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,28 @@ export function selectionClipboard(selection, opts = {}, csv = '') {
|
||||||
return csv ? head + '\n\n' + csv : head;
|
return csv ? head + '\n\n' + csv : head;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `promise` if it settles within `ms`, else `marker`. A rejection still rejects (so a load failure keeps its own
|
||||||
|
* message). Copy uses it so a stalled people-group load can never hold the territory lines hostage: those need
|
||||||
|
* no network, and apply-grouping needs nothing else.
|
||||||
|
*/
|
||||||
|
export function settleWithin(promise, ms, marker) {
|
||||||
|
let timer = 0;
|
||||||
|
const late = new Promise((resolve) => { timer = setTimeout(() => resolve(marker), ms); });
|
||||||
|
return Promise.race([promise, late]).finally(() => clearTimeout(timer));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The error a READER sees for a failed load. One that already carries a catalogue key (keyedError: an HTTP status,
|
||||||
|
* no service configured) passes through, because its text is already true and translated. Anything else (the
|
||||||
|
* request aborted by our own timeout, the network gone, a body that is not JSON) arrives as the browser's raw English
|
||||||
|
* ("signal is aborted without reason"), so it becomes `fallback(e)`, the keyed "still loading, try again" text. The
|
||||||
|
* failed load leaves the cache, so trying again really does fetch again.
|
||||||
|
*/
|
||||||
|
export function asReaderError(e, fallback) {
|
||||||
|
return e && e.i18nKey ? e : fallback(e);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The names a selection's Prayer list and Card deck are titled by: its zones (already localized by the caller), its
|
* The names a selection's Prayer list and Card deck are titled by: its zones (already localized by the caller), its
|
||||||
* countries, and the India states and union territories that BROUGHT people (`from`: slug → the territory names
|
* countries, and the India states and union territories that BROUGHT people (`from`: slug → the territory names
|
||||||
|
|
@ -97,4 +119,4 @@ export function selectionCsvFileName(label) {
|
||||||
return `upg-100-${base}.csv`;
|
return `upg-100-${base}.csv`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { SELECTION_CSV_COLUMNS, csvCell, payloadName, regroupKind, regroupable, selectionCsv, selectionClipboard, selectionTitleNames, selectionCsvFileName };
|
export default { SELECTION_CSV_COLUMNS, csvCell, payloadName, regroupKind, regroupable, selectionCsv, selectionClipboard, settleWithin, asReaderError, selectionTitleNames, selectionCsvFileName };
|
||||||
|
|
|
||||||
36
plugin-designs/my-upg-100-list/data/upg-total.js
vendored
Normal file
36
plugin-designs/my-upg-100-list/data/upg-total.js
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// upg-total.js — how many people groups the UPG-100 list covers, from ONE place: the funnel's last step
|
||||||
|
// (data/people-groups/vision-funnel.json, the same figure the packet's footnote uses). Never write the number into
|
||||||
|
// a string: the list grows (2,106 was printed in eleven languages after it had become 2,107), so every sentence
|
||||||
|
// takes it as {n}, formatted for the reader's locale. check:upg-count keeps it that way.
|
||||||
|
// PURE (no JSON import, so node gates can import it): the .vue callers import the funnel and pass it in.
|
||||||
|
|
||||||
|
/** The list's total: the funnel's last step (the whole DOXA set). 0 when the funnel is missing. */
|
||||||
|
export function upgTotal(funnel) {
|
||||||
|
const steps = (funnel && Array.isArray(funnel.steps)) ? funnel.steps : []
|
||||||
|
return Number(steps.length ? steps[steps.length - 1].groups : 0) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A count as the reader writes numbers (2,107 · 2.107 · 2 107), in `numberingSystem` when one is given (٢٬١٠٧). */
|
||||||
|
export function formatCount(n, locale, numberingSystem) {
|
||||||
|
try { return new Intl.NumberFormat(locale || undefined, numberingSystem ? { numberingSystem } : undefined).format(n) } catch (_) { return String(n) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The non-Western digit system a text already writes its numbers in, or null (Western digits, or none).
|
||||||
|
const DIGIT_SYSTEMS = [['arab', /[\u0660-\u0669]/], ['arabext', /[\u06F0-\u06F9]/], ['deva', /[\u0966-\u096F]/], ['beng', /[\u09E6-\u09EF]/]]
|
||||||
|
export function digitSystemOf(text) {
|
||||||
|
for (const [nu, re] of DIGIT_SYSTEMS) if (re.test(String(text || ''))) return nu
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ ONE SENTENCE, ONE DIGIT SYSTEM. {n} is written in the digits the translated sentence already uses for its own
|
||||||
|
// numbers. CLDR's default for a bare `ar` is Western (Intl.NumberFormat('ar') gives "2,107"), but the ar wall intro
|
||||||
|
// writes "٢٧" and "٩:١٤" in Arabic-Indic, so a plain locale format would put "2,107" between them. The sentence is
|
||||||
|
// rendered once with an empty slot to read its digits, then with the count. Locales whose sentence uses Western
|
||||||
|
// digits (or none) format exactly as before.
|
||||||
|
// `tr` is the caller's t(); passing it in keeps this module pure for the node gate.
|
||||||
|
export function countInSentence(tr, key, n, locale, params = {}) {
|
||||||
|
const bare = tr(key, { ...params, n: '' })
|
||||||
|
return tr(key, { ...params, n: formatCount(n, locale, digitSystemOf(bare)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { upgTotal, formatCount, digitSystemOf, countInSentence }
|
||||||
|
|
@ -46,6 +46,9 @@ import PosterWall from '@map/components/poster/PosterWall.vue'
|
||||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||||
// The reader's language, for the DATA fetch below. Not for interface text — that goes through t().
|
// The reader's language, for the DATA fetch below. Not for interface text — that goes through t().
|
||||||
import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
|
import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
|
||||||
|
// The list's total for the intro, from the funnel (never a number written into a string; see data/upg-total.js).
|
||||||
|
import visionFunnel from '../../data/people-groups/vision-funnel.json'
|
||||||
|
import { upgTotal, countInSentence } from './data/upg-total.js'
|
||||||
// Named imports: only the list arrays ship in the bundle; the file's bookkeeping fields stay out.
|
// Named imports: only the list arrays ship in the bundle; the file's bookkeeping fields stay out.
|
||||||
import { zonesForList } from './data/zones-for-list.js'
|
import { zonesForList } from './data/zones-for-list.js'
|
||||||
// The SAME normalize+evaluate pipeline the sibling map profile uses to turn the live
|
// The SAME normalize+evaluate pipeline the sibling map profile uses to turn the live
|
||||||
|
|
@ -244,7 +247,8 @@ const intro = computed(() => {
|
||||||
// The three plain-text keys come from the shared catalogue (rule 5B). Until they land, the
|
// The three plain-text keys come from the shared catalogue (rule 5B). Until they land, the
|
||||||
// single legacy key is still the source of truth, so this renders identically before AND after: no
|
// single legacy key is still the source of truth, so this renders identically before AND after: no
|
||||||
// flag day, and this consumer does not have to land in step with the catalogue.
|
// flag day, and this consumer does not have to land in step with the catalogue.
|
||||||
const a = t('messages.upg100WallIntroA')
|
// The total is {n}, formatted for the reader (it printed a stale 2,106 in all eleven languages).
|
||||||
|
const a = countInSentence(t, 'upgMap.wallIntroA', upgTotal(visionFunnel), getRuntimeLocale() || detectLocale())
|
||||||
const b = t('messages.upg100WallIntroEmph')
|
const b = t('messages.upg100WallIntroEmph')
|
||||||
const c = t('messages.upg100WallIntroB')
|
const c = t('messages.upg100WallIntroB')
|
||||||
const missing = (v, key) => !v || v === key
|
const missing = (v, key) => !v || v === key
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,9 @@ import { colorForZone } from './data/zone-colors.js'
|
||||||
// clicked tapestry feature carries shapeName (ADM1) OR iso3 (ADM0) and this maps
|
// clicked tapestry feature carries shapeName (ADM1) OR iso3 (ADM0) and this maps
|
||||||
// either to { key, name, count } from admin-counts/membership (NEVER edits them).
|
// either to { key, name, count } from admin-counts/membership (NEVER edits them).
|
||||||
import { resolveTerritory, buildCountryNameMap, formatClipboard, ADM1_COUNTS, ISO3_NAME } from './data/territory-counts.js'
|
import { resolveTerritory, buildCountryNameMap, formatClipboard, ADM1_COUNTS, ISO3_NAME } from './data/territory-counts.js'
|
||||||
import { selectionCsv, selectionClipboard, selectionCsvFileName, regroupable, selectionTitleNames } from './data/selection-export.js'
|
import { selectionCsv, selectionClipboard, selectionCsvFileName, regroupable, settleWithin, asReaderError, selectionTitleNames } from './data/selection-export.js'
|
||||||
|
import visionFunnel from '../../data/people-groups/vision-funnel.json'
|
||||||
|
import { upgTotal, countInSentence } from './data/upg-total.js'
|
||||||
// 09b-search-bar §1(a-c): unified search index (PG + zone + territory) + the
|
// 09b-search-bar §1(a-c): unified search index (PG + zone + territory) + the
|
||||||
// tokenized contains matcher. Pure data + pure fns; this profile builds the rows
|
// tokenized contains matcher. Pure data + pure fns; this profile builds the rows
|
||||||
// from the live records (PG), the legend leafZones (zone), and admin-counts +
|
// from the live records (PG), the legend leafZones (zone), and admin-counts +
|
||||||
|
|
@ -1824,16 +1826,26 @@ function selectedAdm1Names() {
|
||||||
// Copy tap builds its text from cache and writes it inside the tap's user activation (a clipboard write
|
// Copy tap builds its text from cache and writes it inside the tap's user activation (a clipboard write
|
||||||
// after a network wait is refused, on phones above all). A failed load is dropped so the next try refetches.
|
// after a network wait is refused, on phones above all). A failed load is dropped so the next try refetches.
|
||||||
let _selRecords = null; // { lang, promise }
|
let _selRecords = null; // { lang, promise }
|
||||||
|
const RECORDS_TIMEOUT_MS = 30000; // the people-group list: abort a request that has not answered by then
|
||||||
|
const COPY_WAIT_MS = 8000; // Copy waits this long for the people groups, then copies the territories alone
|
||||||
|
// Any load failure without a catalogue key (the timeout's abort, a dropped network) reads as the stall does.
|
||||||
|
const stillLoading = () => keyedError('upgMap.peopleStillLoading', 'the people groups are still loading, try again in a moment');
|
||||||
|
const readerError = (e) => asReaderError(e, stillLoading);
|
||||||
function selectionRecordsLoad() {
|
function selectionRecordsLoad() {
|
||||||
const lang = packetLang();
|
const lang = packetLang();
|
||||||
if (_selRecords && _selRecords.lang === lang) return _selRecords.promise;
|
if (_selRecords && _selRecords.lang === lang) return _selRecords.promise;
|
||||||
if (!API_BASE) return Promise.reject(keyedError('wall.hud.noApi', 'no people-groups service is configured'));
|
if (!API_BASE) return Promise.reject(keyedError('wall.hud.noApi', 'no people-groups service is configured'));
|
||||||
const fields = PRINT_FIELDS; // the print standard's one field list (lists/peopleGroupRecords.js)
|
const fields = PRINT_FIELDS; // the print standard's one field list (lists/peopleGroupRecords.js)
|
||||||
|
// A STALLED request must not hold the cache forever: it aborts after RECORDS_TIMEOUT_MS, rejects, and the catch
|
||||||
|
// below drops it, so the next action fetches again.
|
||||||
|
const ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
||||||
|
const timer = ctrl ? setTimeout(() => ctrl.abort(), RECORDS_TIMEOUT_MS) : 0;
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
const resp = await fetch(`${API_BASE}/api/people-groups/list?fields=${fields}&lang=${lang}`, { cache: 'no-store' });
|
const resp = await fetch(`${API_BASE}/api/people-groups/list?fields=${fields}&lang=${lang}`, { cache: 'no-store', signal: ctrl ? ctrl.signal : undefined })
|
||||||
|
.finally(() => clearTimeout(timer));
|
||||||
if (!resp.ok) throw keyedError('wall.hud.fetchFailed', 'could not load the people groups ({status})', { status: resp.status });
|
if (!resp.ok) throw keyedError('wall.hud.fetchFailed', 'could not load the people groups ({status})', { status: resp.status });
|
||||||
return normalizeRecords(await applyPeopleGroupList(extractRecords(await resp.json()), profileConfig));
|
return normalizeRecords(await applyPeopleGroupList(extractRecords(await resp.json()), profileConfig));
|
||||||
})();
|
})().catch((e) => { throw readerError(e); });
|
||||||
_selRecords = { lang, promise };
|
_selRecords = { lang, promise };
|
||||||
promise.catch(() => { if (_selRecords && _selRecords.promise === promise) _selRecords = null; });
|
promise.catch(() => { if (_selRecords && _selRecords.promise === promise) _selRecords = null; });
|
||||||
return promise;
|
return promise;
|
||||||
|
|
@ -1919,10 +1931,12 @@ function copySelection() {
|
||||||
if (!sel.length) return;
|
if (!sel.length) return;
|
||||||
const color = selectedColor.value;
|
const color = selectedColor.value;
|
||||||
let count = null, note = '';
|
let count = null, note = '';
|
||||||
const text = selectionPeople().then((p) => {
|
const STALLED = {};
|
||||||
|
const text = settleWithin(selectionPeople(), COPY_WAIT_MS, STALLED).then((p) => {
|
||||||
|
if (p === STALLED) { note = t('upgMap.peopleStillLoading'); return selectionClipboard(sel, { color }); }
|
||||||
count = p.records.length; note = selectionNote(p.missing);
|
count = p.records.length; note = selectionNote(p.missing);
|
||||||
return selectionClipboard(sel, { color }, selectionCsv(p.records, p.from, { color }));
|
return selectionClipboard(sel, { color }, selectionCsv(p.records, p.from, { color }));
|
||||||
}, (e) => { note = errorText(e, t); return selectionClipboard(sel, { color }); });
|
}, (e) => { note = errorText(readerError(e), t); return selectionClipboard(sel, { color }); });
|
||||||
text.then((v) => { try { window.__UPG_LAST_CLIPBOARD__ = v } catch (_) {} });
|
text.then((v) => { try { window.__UPG_LAST_CLIPBOARD__ = v } catch (_) {} });
|
||||||
// A selection apply-grouping cannot regroup in one go (a mix of kinds, or a legend zone) still copies; the
|
// A selection apply-grouping cannot regroup in one go (a mix of kinds, or a legend zone) still copies; the
|
||||||
// flash says so, so a partner does not hand an agent a line it will refuse.
|
// flash says so, so a partner does not hand an agent a line it will refuse.
|
||||||
|
|
@ -1980,7 +1994,7 @@ async function exportSelectionCsv() {
|
||||||
const note = selectionNote(p.missing);
|
const note = selectionNote(p.missing);
|
||||||
flashRef(copyFlash, t('upgMap.csvSaved', { count: p.records.length }) + (note ? ' · ' + note.slice(0, 90) : ''), note ? 6000 : 3000);
|
flashRef(copyFlash, t('upgMap.csvSaved', { count: p.records.length }) + (note ? ' · ' + note.slice(0, 90) : ''), note ? 6000 : 3000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
flashRef(copyFlash, t('upgMap.csvFailed', { reason: errorText(e, t).slice(0, 90) }), 5000);
|
flashRef(copyFlash, t('upgMap.csvFailed', { reason: errorText(readerError(e), t).slice(0, 90) }), 5000);
|
||||||
} finally { batchBusy.value = false; busyAction.value = ''; }
|
} finally { batchBusy.value = false; busyAction.value = ''; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2746,7 +2760,7 @@ onMounted(async () => {
|
||||||
// The pins need none of it: they are built from `records` (lat/lng) and slugToZone, and the
|
// The pins need none of it: they are built from `records` (lat/lng) and slugToZone, and the
|
||||||
// selector evaluate() at the pin build maps slugs to zone keys without touching any geometry.
|
// selector evaluate() at the pin build maps slugs to zone keys without touching any geometry.
|
||||||
// 68 KB, arriving at ~2.9 s cold.
|
// 68 KB, arriving at ~2.9 s cold.
|
||||||
status.value = t('messages.fetchingUupgs', { n: 2106 })
|
status.value = countInSentence(t, 'messages.fetchingUupgs', upgTotal(visionFunnel), packetLang()) // the funnel's total, never a literal
|
||||||
const records = normalizeRecords(await applyPeopleGroupList(extractRecords(await recordsPromise), profileConfig))
|
const records = normalizeRecords(await applyPeopleGroupList(extractRecords(await recordsPromise), profileConfig))
|
||||||
// 09b shift-select §1(b): build iso3→display-name from the LIVE records so an
|
// 09b shift-select §1(b): build iso3→display-name from the LIVE records so an
|
||||||
// ADM0 country click resolves to the exact label the legend shows (the ADM0
|
// ADM0 country click resolves to the exact label the legend shows (the ADM0
|
||||||
|
|
|
||||||
|
|
@ -882,7 +882,7 @@
|
||||||
"bundle": "my-upg-100-list",
|
"bundle": "my-upg-100-list",
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
|
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
|
||||||
"integrity": "sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f",
|
"integrity": "sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz",
|
||||||
"elements": [
|
"elements": [
|
||||||
"my-upg-100-list-map"
|
"my-upg-100-list-map"
|
||||||
],
|
],
|
||||||
|
|
@ -905,7 +905,7 @@
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -921,8 +921,8 @@
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" 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<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
|
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" 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<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
|
||||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -938,8 +938,8 @@
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" 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<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
|
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" 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<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
|
||||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -882,7 +882,7 @@
|
||||||
"bundle": "my-upg-100-list",
|
"bundle": "my-upg-100-list",
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
|
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
|
||||||
"integrity": "sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f",
|
"integrity": "sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz",
|
||||||
"elements": [
|
"elements": [
|
||||||
"my-upg-100-list-map"
|
"my-upg-100-list-map"
|
||||||
],
|
],
|
||||||
|
|
@ -905,7 +905,7 @@
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -921,8 +921,8 @@
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" 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<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
|
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" 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<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
|
||||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -938,8 +938,8 @@
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" 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<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
|
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" 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<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
|
||||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||||
<script src="../my-upg-100-list.js" integrity="sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f" crossorigin="anonymous"></script>
|
<script src="../my-upg-100-list.js" integrity="sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||||
<script src="../my-upg-100-list.js" integrity="sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f" crossorigin="anonymous"></script>
|
<script src="../my-upg-100-list.js" integrity="sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||||
<script src="../my-upg-100-list.js" integrity="sha384-SVpZeuorgVIBQxaKjxw3jsEHEZXuF3jxvjoGduL/LNX43z4PsjRCy2rRIbFmE85f" crossorigin="anonymous"></script>
|
<script src="../my-upg-100-list.js" integrity="sha384-w4MZ+fMNdxwE/8sP1J21vjE4GbhaBRqzZWXPRb7zzQcK2YCstJnFY3HbvfqErQNz" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue