Merge main into bundler-2/work
# Conflicts: # public/doxa-maps/country-posters/countries-map/index.html # public/doxa-maps/country-posters/country-poster-wall/index.html # public/doxa-maps/country-posters/country-posters-mfe/index.html # public/doxa-maps/country-posters/country-posters.js # public/doxa-maps/index.html # public/doxa-maps/manifest.json
This commit is contained in:
commit
5516c157bb
70 changed files with 1592 additions and 647 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Made by Vibe Coders for Vibe Coders to prepare the way of the Lord and make His paths straight.
|
||||
|
||||
Isaiah 40:3 “...Prepare the way for the LORD in the wilderness; make a straight highway for our God in the desert."
|
||||
**Isaiah 40:3 “...Prepare the way for the LORD in the wilderness; make a straight highway for our God in the desert."**
|
||||
|
||||
# dx-maps — one build, maps that embed everywhere
|
||||
|
||||
|
|
|
|||
106
internal/check-gospel-composed-filters.mjs
Normal file
106
internal/check-gospel-composed-filters.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* check-gospel-composed-filters.mjs — cell C: the map COMPOSED above its own tables.
|
||||
*
|
||||
* THE CLAIM UNDER TEST is not "it renders". It is that the composite keeps the reader INSIDE
|
||||
* it. Standalone, the sheet's "Every resource in <language>" is a LINK to the tables page.
|
||||
* Composed, it must become an in-place FILTER, because navigating away throws out the map the
|
||||
* reader has just positioned and the group they have just found. Two different affordances
|
||||
* behind the same words, and only one of them is right in each context.
|
||||
*
|
||||
* WHAT IT ASSERTS, and each one is a way the composite can look fine and be broken:
|
||||
* - the control is a <button>, not an <a> (a link here navigates)
|
||||
* - the URL does not change (proves it did not navigate)
|
||||
* - the table's ROW COUNT goes down (proves the filter reached the data)
|
||||
* - the row count stays above zero (proves it filtered rather than emptied)
|
||||
* - the pin count is unchanged (proves the map survived the click)
|
||||
* - no response >= 400 and no page error (proves it did not paper over a failure)
|
||||
*
|
||||
* IT PICKS ITS SUBJECT FROM THE DATA, and that is not a detail. The first run of this check
|
||||
* opened pin 0 — Abai Sungai, a group with NOTHING found — so the filter correctly produced an
|
||||
* empty table and the check failed a working composite. The test was wrong, not the product.
|
||||
* It now selects a tier-2 pin from the live source, so "rows went down but not to zero" is a
|
||||
* question the subject can actually answer.
|
||||
*
|
||||
* BROWSER GATE. Run it under both rendezvous locks, product lock first — the fixed order is
|
||||
* what makes the pair deadlock-free:
|
||||
* flock -w 1200 <product-lock> flock -w 1200 <agent-lock> bun run check:composed-filters
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import { serveReaped } from './_serve-reaped.mjs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const PUBLIC = resolve(HERE, '../public')
|
||||
const PORT = Number(process.env.PORT || 5399)
|
||||
const BASE = process.env.BASE_URL || `http://127.0.0.1:${PORT}`
|
||||
const PATHNAME = '/doxa-maps/gospel-resources/gospel-dashboard/index.html'
|
||||
|
||||
let srv = null
|
||||
if (!process.env.BASE_URL) srv = serveReaped(PUBLIC, PORT)
|
||||
|
||||
const fail = (msg) => { console.error(`COMPOSED-FILTERS: FAIL — ${msg}`); srv?.stop(); process.exit(1) }
|
||||
|
||||
let browser
|
||||
try {
|
||||
browser = await chromium.launch()
|
||||
const p = await browser.newPage({ viewport: { width: 1440, height: 1200 } })
|
||||
const bad = []
|
||||
p.on('response', (r) => { if (r.status() >= 400) bad.push(`${r.status()} ${r.url()}`) })
|
||||
p.on('pageerror', (e) => bad.push(`pageerror: ${String(e.message).slice(0, 120)}`))
|
||||
|
||||
await p.goto(`${BASE}${PATHNAME}`, { waitUntil: 'networkidle', timeout: 90000 })
|
||||
await p.waitForFunction(() => window.__grState && window.__grState().pins > 0, { timeout: 60000 })
|
||||
await p.waitForTimeout(8000)
|
||||
|
||||
const read = () => p.evaluate(() => {
|
||||
const r = document.querySelector('gospel-resources-map').shadowRoot
|
||||
return {
|
||||
pins: window.__grState().rendered,
|
||||
rows: r.querySelectorAll('.tabulator-row').length,
|
||||
url: location.href,
|
||||
chip: (r.querySelector('.gt-filter')?.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 100),
|
||||
}
|
||||
})
|
||||
|
||||
const before = await read()
|
||||
if (!before.rows) fail('the composed tables rendered no rows at all, so a filter proves nothing.')
|
||||
|
||||
const idx = await p.evaluate(() => {
|
||||
const f = window.__grMap.getSource('gr-pin-src')._data.features
|
||||
const hit = f.find((x) => x.properties.tier === 2) || f[0]
|
||||
return hit.properties.i
|
||||
})
|
||||
await p.evaluate((i) => window.__grOpenPin(i), idx)
|
||||
await p.waitForTimeout(2000)
|
||||
|
||||
const clicked = await p.evaluate(() => {
|
||||
const r = document.querySelector('gospel-resources-map').shadowRoot
|
||||
const btn = [...r.querySelectorAll('.gm-tablink')].find((e) => /Every resource in/i.test(e.textContent))
|
||||
if (!btn) return null
|
||||
const tag = btn.tagName.toLowerCase()
|
||||
btn.click()
|
||||
return { tag, text: btn.textContent.trim().slice(0, 60) }
|
||||
})
|
||||
if (!clicked) fail('no "Every resource in ..." control in the group sheet.')
|
||||
if (clicked.tag !== 'button') fail(`the control is a <${clicked.tag}>, not a <button> — composed, it must filter, not navigate.`)
|
||||
|
||||
await p.waitForTimeout(3000)
|
||||
const after = await read()
|
||||
|
||||
console.log(` control : pins=${before.pins} rows=${before.rows}`)
|
||||
console.log(` clicked : <${clicked.tag}> "${clicked.text}"`)
|
||||
console.log(` after : pins=${after.pins} rows=${after.rows}`)
|
||||
console.log(` filter : ${after.chip || '(none)'}`)
|
||||
|
||||
if (after.url !== before.url) fail(`it NAVIGATED (${before.url} -> ${after.url}); the map the reader positioned is gone.`)
|
||||
if (after.pins !== before.pins) fail(`the map lost pins on the click: ${before.pins} -> ${after.pins}.`)
|
||||
if (!(after.rows < before.rows)) fail(`the table did not narrow: ${before.rows} -> ${after.rows}.`)
|
||||
if (after.rows === 0) fail('the table emptied rather than filtered — subject had no resources, so this proves nothing.')
|
||||
if (bad.length) fail(`${bad.length} failed request(s) or page error(s): ${[...new Set(bad)].slice(0, 3).join(' | ')}`)
|
||||
|
||||
console.log('COMPOSED-FILTERS: PASS — filters in place, keeps the map and the URL, no failed requests.')
|
||||
} finally {
|
||||
await browser?.close()
|
||||
srv?.stop()
|
||||
}
|
||||
|
|
@ -203,6 +203,8 @@ async function measure(width, lang) {
|
|||
const boxes = cards.map((c) => ({ c, b: c.getBoundingClientRect() }))
|
||||
let intrusions = 0, overflowing = 0, worst = 0, coveringPreview = 0, worstY = 0
|
||||
let previewsPainted = 0, previewsBroken = 0, noPreview = 0, unnamedPreview = 0
|
||||
let notTaped = 0, worstTape = 0, selectsInRow = 0
|
||||
let headNotTaped = 0, worstHead = 0, mapCovered = 0, worstCover = 0
|
||||
for (const { c, b } of boxes) {
|
||||
const row = c.querySelector('.pfs')
|
||||
if (!row) continue
|
||||
|
|
@ -247,6 +249,69 @@ async function measure(width, lang) {
|
|||
worstY = Math.max(worstY, Math.round(ib.bottom - rb.top))
|
||||
}
|
||||
}
|
||||
// ★ A STICKER STRADDLES THE EDGE IT TAPES (Driver 2026-09-25, the sticker/banner brief).
|
||||
// The card is a BANNER displaying a map; each sticker sits centred on a banner edge, part
|
||||
// on the banner's own white space and part overhanging. Three things were each true while
|
||||
// this gate was green, so all three are now asserted:
|
||||
// 1. the row floated 26px BELOW the banner, touching nothing (the shipped state);
|
||||
// 2. the row sat entirely below the edge, touching at one pixel;
|
||||
// 3. the row straddled correctly but landed ON THE MAP — "offensive to the map".
|
||||
// over-preview above catches only a row overlapping the <img> from above.
|
||||
const frame = c.querySelector('.main')
|
||||
if (frame) {
|
||||
const fb = frame.getBoundingClientRect()
|
||||
// STRADDLE: some of the sticker on the banner, some off it. Requiring a near-even split
|
||||
// would encode a ratio the Driver explicitly left open ("probably an equal or more
|
||||
// overlap"); requiring BOTH sides to be non-trivial is the property actually stated.
|
||||
const onBanner = Math.round(fb.bottom - rb.top)
|
||||
const offBanner = Math.round(rb.bottom - fb.bottom)
|
||||
if (onBanner < 4 || offBanner < 4) {
|
||||
notTaped++
|
||||
const d = onBanner < 4 ? onBanner : -offBanner
|
||||
worstTape = Math.abs(d) > Math.abs(worstTape) ? d : worstTape
|
||||
}
|
||||
}
|
||||
// ★ NO NATIVE DROPDOWN IN THE DEFAULT ROW (same ruling). The Driver's reason is not
|
||||
// cosmetic: "I don't want to add the dropdown for the card because it makes it hard for
|
||||
// others to have dropdowns too" — a <select> here collides with the host page's own
|
||||
// selects. The size picker is a disclosure, revealed by the Card Deck button. A <select>
|
||||
// reappearing in .pfs is the regression this catches.
|
||||
if (row.querySelector('select')) selectsInRow++
|
||||
// ★ AND THE HEADER STICKER STRADDLES ITS EDGE TOO. Same requirement, other end of the
|
||||
// banner, and it was ALSO wrong while everything here was green: MEASURED 35px above the
|
||||
// banner's top edge and -1px below, i.e. floating over the card touching the banner
|
||||
// nowhere. Asserting only the footer would have left that in place indefinitely.
|
||||
const headSticker = c.querySelector('.tape-h')
|
||||
if (frame && headSticker) {
|
||||
const hb = headSticker.getBoundingClientRect()
|
||||
const fb2 = frame.getBoundingClientRect()
|
||||
const onB = Math.round(hb.bottom - fb2.top)
|
||||
const offB = Math.round(fb2.top - hb.top)
|
||||
if (onB < 4 || offB < 4) {
|
||||
headNotTaped++
|
||||
const d = onB < 4 ? onB : -offB
|
||||
worstHead = Math.abs(d) > Math.abs(worstHead) ? d : worstHead
|
||||
}
|
||||
}
|
||||
// ★ NOTHING TOUCHES THE MAP. The brief is unambiguous and is the reason the banner grew
|
||||
// at all: "the preview banner to showcase a map should not have anything overlapping the
|
||||
// map because the map needs to be fully visible." Measured on the first straddle attempt:
|
||||
// header over the map by 6px, footer by 8px — a correct straddle onto a banner with too
|
||||
// little white space reserved. THIS is the assertion that catches that, and neither the
|
||||
// straddle checks above nor over-preview can: over-preview only sees a row whose top is
|
||||
// above the image bottom, which is a different geometry from a sticker landing on it.
|
||||
if (img) {
|
||||
const ib3 = img.getBoundingClientRect()
|
||||
if (ib3.height > 2) {
|
||||
const byFoot = Math.round(ib3.bottom - rb.top)
|
||||
if (byFoot > 0) { mapCovered++; worstCover = Math.max(worstCover, byFoot) }
|
||||
if (headSticker) {
|
||||
const hb2 = headSticker.getBoundingClientRect()
|
||||
const byHead = Math.round(hb2.bottom - ib3.top)
|
||||
if (byHead > 0) { mapCovered++; worstCover = Math.max(worstCover, byHead) }
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const o of boxes) {
|
||||
if (o.c === c) continue
|
||||
const ox = Math.min(rb.right, o.b.right) - Math.max(rb.left, o.b.left)
|
||||
|
|
@ -295,7 +360,8 @@ async function measure(width, lang) {
|
|||
return { cards: cards.length, rows: cards.filter((c) => c.querySelector('.pfs')).length,
|
||||
overflowing, worst, intrusions, coveringPreview, worstY,
|
||||
previewsPainted, previewsBroken, noPreview, unnamedPreview, clipped: clipped.length,
|
||||
controls: ctrls.length, tiny: tiny.length, perRow, cardW }
|
||||
controls: ctrls.length, tiny: tiny.length, perRow, cardW,
|
||||
notTaped, worstTape, selectsInRow, headNotTaped, worstHead, mapCovered, worstCover }
|
||||
})()`)
|
||||
await page.close()
|
||||
return { width, lang, hover, reach, ...out }
|
||||
|
|
@ -315,6 +381,10 @@ for (const r of results) {
|
|||
+ ` · overflow ${r.overflowing}${r.worst ? ` (${r.worst}px)` : ''}`
|
||||
+ ` · neighbour ${r.intrusions}`
|
||||
+ ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}`
|
||||
+ ` · off-frame ${r.notTaped}${r.worstTape ? ` (${r.worstTape > 0 ? '+' : ''}${r.worstTape}px)` : ''}`
|
||||
+ ` · selects ${r.selectsInRow}`
|
||||
+ ` · head-off ${r.headNotTaped}${r.worstHead ? ` (${r.worstHead > 0 ? '+' : ''}${r.worstHead}px)` : ''}`
|
||||
+ ` · map-covered ${r.mapCovered}${r.worstCover ? ` (${r.worstCover}px)` : ''}`
|
||||
+ ` · previews ${r.previewsPainted} painted`
|
||||
+ `${r.previewsBroken ? `/${r.previewsBroken} BROKEN` : ''}${r.noPreview ? `/${r.noPreview} none` : ''}`
|
||||
+ ` · max ${r.perRow}/row @ ${r.cardW}px`
|
||||
|
|
@ -362,6 +432,39 @@ for (const r of results) {
|
|||
fails.push(`${at}: ${r.previewsBroken} preview image(s) failed to load (naturalWidth 0) — `
|
||||
+ 'a broken preview is not something the geometry checks can see')
|
||||
}
|
||||
// ★ THE OTHER EDGE OF over-preview. That assertion catches a row sitting too HIGH; this one
|
||||
// catches it sitting too LOW, which is the state that actually shipped and that nothing here
|
||||
// could see: MEASURED frame bottom 426, row top 452, and a full green run.
|
||||
if (r.notTaped) {
|
||||
fails.push(`${at}: ${r.notTaped} print row(s) do not STRADDLE the banner's bottom edge `
|
||||
+ `(worst ${r.worstTape > 0 ? '+' : ''}${r.worstTape}px on one side; positive = too little `
|
||||
+ 'on the banner, negative = too little overhanging). The footer sticker sits centred on '
|
||||
+ 'that edge, part on the banner\'s white space and part hanging onto the page below — '
|
||||
+ '"slight overlap over the banner, but not over the content of the banner". A row '
|
||||
+ 'floating clear of the banner is as wrong as one covering the map.')
|
||||
}
|
||||
// ★ THE ONE THE DRIVER CARES ABOUT MOST. "The preview banner to showcase a map should not
|
||||
// have anything overlapping the map because the map needs to be fully visible."
|
||||
if (r.mapCovered) {
|
||||
fails.push(`${at}: ${r.mapCovered} sticker(s) OVERLAP the map preview (worst ${r.worstCover}px). `
|
||||
+ 'The banner\'s header and footer stickers straddle the banner\'s edges; the map inside is '
|
||||
+ 'never to be touched — "if I put a piece of tape over the text it would be offensive to '
|
||||
+ 'the map". Reserve more banner white space via --banner-pad-top/--banner-pad-bottom on '
|
||||
+ '.main rather than moving the sticker: a sticker centred on the edge puts HALF its '
|
||||
+ 'height on the banner, so the padding must exceed that half.')
|
||||
}
|
||||
if (r.headNotTaped) {
|
||||
fails.push(`${at}: ${r.headNotTaped} header sticker(s) do not straddle the banner's top edge `
|
||||
+ `(worst ${r.worstHead > 0 ? '+' : ''}${r.worstHead}px on one side). A sticker sits ON the `
|
||||
+ 'edge it tapes, part on the banner and part overhanging — measured 35px above / -1px '
|
||||
+ 'below before this was asserted, i.e. floating free of the banner entirely.')
|
||||
}
|
||||
if (r.selectsInRow) {
|
||||
fails.push(`${at}: ${r.selectsInRow} native <select>(s) in the print row. The card-stock `
|
||||
+ 'picker is a DISCLOSURE behind the Card Deck button, not a control in the default row — '
|
||||
+ '"I don\'t want to add the dropdown for the card because it makes it hard for others to '
|
||||
+ 'have dropdowns too." A <select> here also collides with the host page\'s own dropdowns.')
|
||||
}
|
||||
// EVERY card falling back to the placeholder means the previews are gone. MEASURED by aborting
|
||||
// every previews/** request: the product degrades WELL — all 27 cards swap to a .ph placeholder
|
||||
// reading "poster not generated yet", with no broken-image icons and no <img> left at all. That
|
||||
|
|
|
|||
|
|
@ -72,6 +72,13 @@ const MARKERS = [
|
|||
component: 'library/composables/useMapInstance.js',
|
||||
sourceRe: /if \(!element\) return\n\s*const root = element\.getRootNode\(\)/,
|
||||
builtRe: /if\(!([\w$]+)\)return;const ([\w$]+)=\1\.getRootNode\(\);if\(!\(\2 instanceof ShadowRoot\)\)return/,
|
||||
// ★ NOT EVERY CONSUMER COMPILES THE GUARDED FUNCTION. ensureEngineCSS is only called by
|
||||
// initializeMap; a bundle that imports pieces of useMapInstance (ensureEngineScript, engineGlobal…)
|
||||
// but never initializeMap gets it TREE-SHAKEN AWAY, so the guard can never appear there — and the
|
||||
// race cannot happen there either. Reported by upg100 (my-upg-100-list) 2026-09-25: 3 of 12
|
||||
// bundles were false negatives. `staleRe` is the UNGUARDED shape: a bundle is MISSING the fix only
|
||||
// if it still compiles ensureEngineCSS without the guard; with neither shape it is NOT AFFECTED.
|
||||
staleRe: /const ([\w$]+)=([\w$]+)\.getRootNode\(\);if\(!\(\1 instanceof ShadowRoot\)\)return/g,
|
||||
marker: 'if (!element) return (ensureEngineCSS)',
|
||||
landed: 'studio, 2026-09-25',
|
||||
why: 'a map whose pane unmounts mid-initialise no longer throws "reading getRootNode" (check-map-unmount-mid-init)',
|
||||
|
|
@ -80,6 +87,13 @@ const MARKERS = [
|
|||
// A marker is a literal (`marker`, matched with includes) unless the entry supplies regexes.
|
||||
const inSource = (m, src) => (m.sourceRe ? m.sourceRe.test(src) : src.includes(m.marker))
|
||||
const inBuilt = (m, js) => (m.builtRe ? m.builtRe.test(js) : js.includes(m.marker))
|
||||
// With a staleRe: does the bundle compile the OLD shape — an occurrence NOT immediately preceded by
|
||||
// the guard? (The guarded form contains the old shape as a substring, hence the look-behind.)
|
||||
const hasStale = (m, js) => {
|
||||
if (!m.staleRe) return true
|
||||
for (const x of js.matchAll(new RegExp(m.staleRe.source, 'g'))) if (!/return;$/.test(js.slice(Math.max(0, x.index - 24), x.index))) return true
|
||||
return false
|
||||
}
|
||||
|
||||
if (!existsSync(TREE)) {
|
||||
console.log(`SHARED-REACHED-BUNDLES: SKIP — no built tree at ${TREE}.`)
|
||||
|
|
@ -158,19 +172,23 @@ for (const m of MARKERS) {
|
|||
const have = []
|
||||
const missing = []
|
||||
const unbuilt = []
|
||||
const unaffected = []
|
||||
for (const bundle of consumers) {
|
||||
const built = join(TREE, bundle, `${bundle}.js`)
|
||||
if (!existsSync(built)) { unbuilt.push(bundle); continue }
|
||||
const js = readFileSync(built, 'utf8')
|
||||
;(inBuilt(m, js) ? have : missing).push(bundle)
|
||||
if (inBuilt(m, js)) have.push(bundle)
|
||||
else if (hasStale(m, js)) missing.push(bundle)
|
||||
else unaffected.push(bundle)
|
||||
}
|
||||
|
||||
console.log(` ${m.id} — ${m.why}`)
|
||||
console.log(` landed ${m.landed}; ${have.length} of ${have.length + missing.length} built `
|
||||
+ `consumer(s) carry it${unbuilt.length ? `, ${unbuilt.length} not built` : ''}`)
|
||||
+ `consumer(s) carry it${unaffected.length ? ` (${unaffected.length} not affected)` : ''}${unbuilt.length ? `, ${unbuilt.length} not built` : ''}`)
|
||||
if (have.length) console.log(` ✓ ${have.join(', ')}`)
|
||||
if (missing.length) console.log(` ✗ ${missing.join(', ')}`)
|
||||
if (unbuilt.length) console.log(` · not built: ${unbuilt.join(', ')}`)
|
||||
if (unaffected.length) console.log(` – not affected (the changed code is not compiled in): ${unaffected.join(', ')}`)
|
||||
|
||||
if (missing.length) {
|
||||
fails.push(`${m.id}: ${missing.length} bundle(s) do NOT carry "${m.why}" — ${missing.join(', ')}`)
|
||||
|
|
|
|||
|
|
@ -469,18 +469,17 @@ const tabsForMobile = computed(() => {
|
|||
const treeTitleForMobile = computed(() => {
|
||||
// A profile-supplied title names the MAP; the taxonomy names only the tab.
|
||||
if (props.title) return props.title
|
||||
if (_isFlatTabMobile.value) return 'Legend'
|
||||
if (props.legendType === 'affinity-block') return 'People Groups'
|
||||
if (props.legendType === 'doxa-regions') return 'WAGF Regions'
|
||||
return 'Languages'
|
||||
if (_isFlatTabMobile.value) return t('legend.titleBar.legend')
|
||||
if (props.legendType === 'affinity-block') return t('legend.titleBar.peopleGroups')
|
||||
if (props.legendType === 'doxa-regions') return t('legend.titleBar.wagfRegions')
|
||||
return t('legend.titleBar.languages')
|
||||
})
|
||||
const treeColumnLabelForMobile = computed(() => {
|
||||
if (props.columnLabel !== null) return props.columnLabel
|
||||
if (props.legendType === 'religion') return 'UUPG Religions'
|
||||
if (props.legendType === 'prayer') return 'Prayer Status'
|
||||
if (props.legendType === 'engagement') return 'Engagement Status'
|
||||
if (props.legendType === 'adoption') return 'Adoption Status'
|
||||
if (_isFlatTabMobile.value) return 'Status'
|
||||
// From the catalog (legend.colLabel.*), translated in all 11 locales; these were English
|
||||
// literals, so a phone in ar/zh/ru/hi showed "Prayer Status" over translated rows.
|
||||
if (['religion', 'prayer', 'engagement', 'adoption'].includes(props.legendType)) return t('legend.colLabel.' + props.legendType)
|
||||
if (_isFlatTabMobile.value) return t('legend.colLabel.status')
|
||||
return ''
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -187,9 +187,28 @@ function storePerRow(n) {
|
|||
}
|
||||
// grid-template-columns, or nothing at all when the count is 0 — an empty string leaves the
|
||||
// stylesheet's auto-fill rule in charge rather than overriding it with something equivalent.
|
||||
const gridStyle = computed(() => perRow.value > 0
|
||||
? { gridTemplateColumns: `repeat(${perRow.value}, minmax(0, 1fr))` }
|
||||
: {})
|
||||
// ⚠ THE PREFERENCE IS CLAMPED BY WHAT THE VIEWPORT CAN ACTUALLY HOLD.
|
||||
// An inline style beats the stylesheet, so `repeat(2, minmax(0,1fr))` overrode the CSS rule's
|
||||
// `minmax(210px,1fr)` at EVERY width — including 390px, where two columns give a ~162px card.
|
||||
// Reported from a 390x844 phone shot: two posters per row, where the CSS alone would have
|
||||
// given one. The stored preference is the Driver's and must survive; it is a preference for
|
||||
// DESKTOP density, not an instruction to overflow a phone.
|
||||
// 210px is the same minimum the stylesheet's auto-fill uses, so the clamp agrees with the rule
|
||||
// it is overriding rather than inventing a second breakpoint.
|
||||
const CARD_MIN_PX = 210
|
||||
const viewportW = ref(typeof window !== 'undefined' ? window.innerWidth : 1280)
|
||||
if (typeof window !== 'undefined') {
|
||||
const onResize = () => { viewportW.value = window.innerWidth }
|
||||
window.addEventListener('resize', onResize)
|
||||
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
||||
}
|
||||
const gridStyle = computed(() => {
|
||||
if (!(perRow.value > 0)) return {}
|
||||
// The wall's own padding either side; err toward FEWER columns rather than a clipped card.
|
||||
const usable = Math.max(0, viewportW.value - 36)
|
||||
const fits = Math.max(1, Math.floor(usable / CARD_MIN_PX))
|
||||
return { gridTemplateColumns: `repeat(${Math.min(perRow.value, fits)}, minmax(0, 1fr))` }
|
||||
})
|
||||
|
||||
const plural = computed(() => props.itemNounPlural || `${props.itemNoun}s`)
|
||||
const base = computed(() => String(props.assetsBase || './assets').replace(/\/+$/, ''))
|
||||
|
|
@ -409,6 +428,31 @@ const prints = usePosterPrints({
|
|||
const canPrintRows = computed(() => typeof props.records === 'function')
|
||||
const previewIsControl = computed(() => props.previewAction === 'print')
|
||||
|
||||
// ── CARD-DECK DISCLOSURE ────────────────────────────────────────────────────
|
||||
// Which card currently has its size row revealed, by item id. Holding an ID
|
||||
// rather than a boolean-per-card means opening one CLOSES the others for free:
|
||||
// two size rows open at once would overlap in the grid gap, and there is no
|
||||
// reading of the Driver's request where two are wanted.
|
||||
const deckOpenFor = ref(null)
|
||||
function toggleDeck(id, ev) {
|
||||
deckOpenFor.value = deckOpenFor.value === id ? null : id
|
||||
// Tell the size row how far down to start: the print row's REAL height, which is
|
||||
// one line in en and two in the long-label locales. Walking up from the clicked
|
||||
// button to its own .pfs keeps this correct per card rather than per wall.
|
||||
const pfs = ev?.currentTarget?.closest?.('.pfs')
|
||||
if (pfs) pfs.parentElement?.style.setProperty('--pstocks-gap', `${pfs.offsetHeight}px`)
|
||||
}
|
||||
|
||||
// Choosing a size IS the print command — the reader already said "print the
|
||||
// cards" by opening this row, so a second confirm step would be a click with
|
||||
// nothing behind it. prints.stock reads pickedStock through a getter, so setting
|
||||
// it here is what the deck prints at.
|
||||
function printDeckAt(item, stockId) {
|
||||
pickedStock.value = stockId
|
||||
deckOpenFor.value = null
|
||||
prints.printCardDeck(item)
|
||||
}
|
||||
|
||||
function downloadPacket(item) {
|
||||
// Still the honest "not yet" when this wall has no records provider: NOT an
|
||||
// error — unbuilt, not broken. phase:'error' reads to a user as "something went
|
||||
|
|
@ -479,12 +523,21 @@ function download(blob, name) {
|
|||
function dismissExport() { exportState.value = null }
|
||||
|
||||
useShadowStyles(
|
||||
'.wall{position:absolute;inset:0;overflow-y:auto;padding:22px 18px 44px;' +
|
||||
// --wall-pad-top is read back by .sec-head::before to cover the scroll-padding strip
|
||||
// above a pinned header (see there). Declared as a var so the two can never drift:
|
||||
// typing 22px in both places is how that gap would silently re-open.
|
||||
'.wall{--wall-pad-top:22px;position:absolute;inset:0;overflow-y:auto;' +
|
||||
'padding:var(--wall-pad-top) 18px 44px;' +
|
||||
"background:#f6f7f9;color:#0f1419;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}" +
|
||||
'.wall.is-dark{background:#0a1410;color:#e5e7eb}' +
|
||||
// Embedded: normal flow, no pane background, no pane padding. inset:auto matters as much
|
||||
// as position:static — leaving inset set keeps the box stretched even once static.
|
||||
'.wall--embedded{position:static;inset:auto;overflow-y:visible;padding:0;background:transparent}' +
|
||||
// ⚠ --wall-pad-top MUST go to 0 here too, not just the padding. Embedded, the wall is not
|
||||
// the scroll container — the HOST page is — so there is no scroll-padding strip to cover,
|
||||
// and a 22px cover bar would paint the wall's background over the host's own content
|
||||
// above each header. The var and the padding are one decision, set in one place.
|
||||
'.wall--embedded{--wall-pad-top:0px;position:static;inset:auto;overflow-y:visible;' +
|
||||
'padding:0;background:transparent}' +
|
||||
'.wall--embedded.is-dark{background:transparent}' +
|
||||
'.wall-h{margin:0 0 6px;font-size:19px;font-weight:700;text-align:center}' +
|
||||
'.wall-tag{margin:0 auto 12px;width:fit-content;padding:3px 11px;border-radius:999px;' +
|
||||
|
|
@ -497,11 +550,16 @@ useShadowStyles(
|
|||
// GAP must clear the OVERHANGING TAPES, not just the card boxes: the header tape
|
||||
// sits at top:-11px and the footer fieldset at bottom:-14px, so a 26px row gap
|
||||
// left ~1px of real air and the wall read as one dense block.
|
||||
// ⚠ THE ROW GAP MUST CLEAR THE PRINT ROW. .pfs hangs BELOW its card (top:100%, margin-top:-14px),
|
||||
// so the vertical gap is what keeps it off the next card. 64px was tuned when that row was one
|
||||
// line at 35px; wrapping it to two lines takes 78px and needs 78-14=64 — exactly the gap, with
|
||||
// nothing spare, which showed up as 5 rows intruding into the card below. 88px leaves real room.
|
||||
'.grid{display:grid;gap:88px 30px;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));' +
|
||||
// ⚠ THE ROW GAP MUST CLEAR THE PRINT ROW, and 88px was tuned for a row that NO LONGER HANGS
|
||||
// THAT FAR. It was set when .pfs sat at top:100% with margin-top:-14px on the CARD — i.e. the
|
||||
// whole row (up to 78px wrapped) hung below the card and the gap had to clear all of it.
|
||||
// The row now straddles the BANNER's bottom edge and overhangs by --tape-overlap plus whatever
|
||||
// grows downward, and the card's own 26px bottom padding absorbs part of that. Reported from a
|
||||
// 390x844 phone shot: a ~180px empty band between one row's print stickers and the next row's
|
||||
// header stickers — the gap is now clearing a distance nothing occupies.
|
||||
// 56px still clears a two-line row (the ro/ru case) with room; the assertion that keeps this
|
||||
// honest is `neighbour 0` in check-poster-card-fits, which fails if a row reaches another card.
|
||||
'.grid{display:grid;gap:56px 30px;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));' +
|
||||
'max-width:1180px;margin:0 auto}' +
|
||||
// The per-row control. Sits above its own grid, right-aligned so it reads as a setting for the
|
||||
// block below rather than as content, and inherits the wall's type so it does not announce
|
||||
|
|
@ -513,8 +571,13 @@ useShadowStyles(
|
|||
'border-radius:6px;background:#fff;color:inherit;cursor:pointer}' +
|
||||
'.is-dark .perrow__label{color:#9fb3a6}' +
|
||||
'.is-dark .perrow__select{background:#132019;border-color:rgba(110,231,183,.20)}' +
|
||||
'.card{position:relative;background:#fff;border:1px solid #e3e6ea;border-radius:3px;' +
|
||||
'padding:24px 12px 26px;box-shadow:0 1px 3px rgba(0,0,0,.10),0 6px 18px rgba(0,0,0,.07);' +
|
||||
// --card-pad-top IS the banner's top edge in card coordinates, and .tape-h reads it to
|
||||
// centre itself there. Declared as a var so the two cannot drift apart the way a second
|
||||
// hand-typed 24px would.
|
||||
'.card{--card-pad-top:24px;--tape-overlap:16px;position:relative;background:#fff;border:1px solid #e3e6ea;' +
|
||||
'border-radius:3px;' +
|
||||
'padding:var(--card-pad-top) 12px 26px;' +
|
||||
'box-shadow:0 1px 3px rgba(0,0,0,.10),0 6px 18px rgba(0,0,0,.07);' +
|
||||
'transition:transform .16s ease,box-shadow .16s ease}' +
|
||||
// ★ THE BLUE OUTLINE IS A HOVER STATE, NOT A MISSING STATIC BORDER. Several reports have asked
|
||||
// for "the blue outline around the poster" to be restored, and MEASURED 2026-09-24 there has
|
||||
|
|
@ -546,7 +609,21 @@ useShadowStyles(
|
|||
'font-size:10.5px;font-weight:600;letter-spacing:.02em;cursor:pointer;' +
|
||||
'border:1px solid rgba(0,0,0,.08);background:#eef2f6;color:#23303c}' +
|
||||
'.is-dark .tape{background:#1d2b23;color:#cfe9d9;border-color:rgba(255,255,255,.10)}' +
|
||||
'.tape-h{top:-11px;width:82%;padding:4px 8px;border-radius:999px;' +
|
||||
// ★ THE HEADER STICKER STRADDLES THE BANNER'S TOP EDGE, for the same reason the footer one
|
||||
// straddles the bottom. MEASURED before: 35px above the banner's top edge, -1px below — it
|
||||
// floated over the card, touching the banner nowhere. The Driver reads the rounded top
|
||||
// sticker as the one that "looks beautiful and is really well spaced", so this keeps its
|
||||
// size and shape exactly and only corrects WHERE it sits: centred on the edge it is taping.
|
||||
// --banner-pad-top reserves the banner surface it lands on, so it never reaches the map.
|
||||
// .tape-h is positioned against .card (it is a direct child), and the banner's top is the
|
||||
// card's padding-top — hence top:24px, read from the card rather than restated.
|
||||
// Same rule as .pfs below: the OVERLAP onto the banner is the constant, not the centre. This
|
||||
// sticker is a single line at every width so centring happened to work, but pinning both
|
||||
// stickers the same way means one --tape-overlap governs both and neither can drift.
|
||||
// Its height grows UPWARD here (bottom-anchored to the band) so it never reaches the map.
|
||||
'.tape-h{top:calc(var(--card-pad-top,24px) - var(--tape-overlap,16px));' +
|
||||
'transform:translateX(-50%);' +
|
||||
'width:82%;padding:4px 8px;border-radius:999px;' +
|
||||
'display:flex;align-items:center;justify-content:center;gap:5px}' +
|
||||
'.tape-h:hover{background:#e2e8ee}' +
|
||||
'.hcount{font-weight:800;font-variant-numeric:tabular-nums}' +
|
||||
|
|
@ -570,20 +647,43 @@ useShadowStyles(
|
|||
// a vertical one the driver saw. Anchoring at top:100% pins it just under the card, so the
|
||||
// second line grows DOWN into empty space. MEASURED: 92px between the preview's bottom and
|
||||
// the next card, of which the single-line row used 35 — 51px spare.
|
||||
// ★ THE FOOTER IS TAPE TOO (Driver): "see how the header for show on map is inline on the top of
|
||||
// the poster box? but the footer print options are not inline on the border or outline of the
|
||||
// box... we want the footer to imitate the header, by being inline like a piece of tape".
|
||||
// The header does that with .tape-h{top:-11px}: a fixed offset that works because its height is
|
||||
// fixed. The footer's height is NOT fixed -- it holds two to four controls plus a select, and it
|
||||
// grows with translated labels -- so a fixed margin-top:-14px left it hanging BELOW the edge
|
||||
// instead of sitting on it, and any height change moved it again.
|
||||
// translate(-50%,-50%) at top:100% centres it on the bottom border at ANY height, which is the
|
||||
// same "piece of tape" the header achieves and is self-correcting when a locale makes it taller.
|
||||
// That also answers the rest of the Driver's rule -- "if the box is too tall to centre we just
|
||||
// let it take up an overlap of the poster box, but not so far that it overlaps the poster blue
|
||||
// outline": half the tape overlaps the card and half hangs below, so the overlap grows only half
|
||||
// as fast as the tape does and never reaches the preview.
|
||||
'.pfs{position:absolute;top:100%;left:50%;transform:translate(-50%,-50%);' +
|
||||
// ★ THE FOOTER STICKER STRADDLES THE BANNER'S BOTTOM EDGE (Driver): "see how the header for
|
||||
// show on map is inline on the top of the poster box? but the footer print options are not
|
||||
// inline on the border or outline of the box... we want the footer to imitate the header, by
|
||||
// being inline like a piece of tape".
|
||||
//
|
||||
// top:100% + translate(-50%,-50%) centres it on that edge at ANY height, which matters because
|
||||
// the row's height is NOT fixed: it holds two to four controls and grows with translated
|
||||
// labels (MEASURED 68px in en/ro, 54px in zh). A fixed margin-top left it hanging below the
|
||||
// edge and moved again whenever the height changed. Centring is self-correcting, so no
|
||||
// constant here has to track the locale.
|
||||
//
|
||||
// ⚠ top:100% IS THE BANNER'S BOTTOM EDGE ONLY BECAUSE .pfs LIVES INSIDE .main. As a SIBLING
|
||||
// of the banner it resolved against the CARD instead — MEASURED card bottom 453 vs banner
|
||||
// bottom 426, so the row centred on a line 27px past the banner, touching it nowhere. The
|
||||
// re-parenting and the centring are one fix; neither works alone.
|
||||
//
|
||||
// Half the sticker lands on the banner, so the banner must reserve white space for it or the
|
||||
// sticker sits on the map: that is --banner-pad-bottom on .main, and the gate asserts the
|
||||
// result (map-covered 0) rather than trusting the arithmetic.
|
||||
//
|
||||
// ⚠ DO NOT ADD margin-top HERE. `margin:0` in the shorthand below RESETS any margin-top
|
||||
// declared above it — a shorthand always beats an earlier longhand in the same block. A
|
||||
// margin-top:-1px sat here computing to 0px while I believed it was doing the positioning;
|
||||
// a mutation test (set it to 26px, expect the row to move) proved it inert.
|
||||
// ⚠ NOT CENTRED — and this is a MEASURED correction, not a preference. translate(-50%,-50%)
|
||||
// puts HALF the row's height onto the banner, and the row's height is CONTENT-DEPENDENT: it
|
||||
// wraps to two lines in ro/ru and to three at 390px once the labels wrap. Measured ro/390px:
|
||||
// 27 of 27 stickers overlapping the map by 4px, while en and zh were clean at the same
|
||||
// padding. No fixed --banner-pad-bottom can be right for every locale x width, because half
|
||||
// of a variable is a variable.
|
||||
//
|
||||
// So the OVERLAP is the constant instead of the centre: the sticker's top is pinned a fixed
|
||||
// --tape-overlap above the banner's bottom edge, and any extra height grows DOWNWARD into
|
||||
// the grid's row gap, where there is room. The amount sitting on the banner is now the same
|
||||
// in all 11 locales at every width, which is what --banner-pad-bottom has to clear.
|
||||
'.pfs{position:absolute;top:calc(100% - var(--tape-overlap,16px));' +
|
||||
'left:50%;transform:translateX(-50%);' +
|
||||
// ⚠ min-width:0 ON THE FIELDSET ITSELF. A <fieldset> gets `min-width:min-content` from the
|
||||
// UA stylesheet, which OVERRIDES width and max-width — measured: computed width 431px
|
||||
// inside a 212px card while the rule said 82%. That is the real reason `width:82%` read as
|
||||
|
|
@ -599,13 +699,30 @@ useShadowStyles(
|
|||
// of 108 controls clipped. Fitting a row by hiding what its buttons say is not fitting it.
|
||||
// flex-basis:auto + wrap lets each control take the width its LABEL needs and flow to a
|
||||
// second line, which there is room for now that the row grows downward.
|
||||
'.pfs > .pbtn{flex:0 1 auto;min-width:0;white-space:nowrap}' +
|
||||
// ⚠ THE <select> IS THE ONE THAT WILL NOT SHRINK. Measured: with min-width:0 applied to
|
||||
// every child, .pstock still rendered 188px inside a 198px row because a <select> sizes to
|
||||
// its WIDEST OPTION and `flex:0 1 auto` keeps that as its basis. flex-basis:0 makes it share
|
||||
// the row like the buttons do; a cap keeps it from dominating the three text controls.
|
||||
'.pfs > .pstock{flex:0 1 auto;min-width:0;max-width:100%}' +
|
||||
// ⚠ nowrap HERE IS A CLIPPING BUG IN THE NARROW x LONG-LABEL CORNER, and it only appeared once
|
||||
// the per-row default became 2: at 390px that halves the card to ~170px, and Romanian's
|
||||
// "Pachet de cartonașe" (133px) plus two more controls cannot share it on one line.
|
||||
// MEASURED after the merge: 54 of 81 controls clipped at ro/390px, en and zh clean — long
|
||||
// labels and a narrow card, exactly the pair each change was individually fine with.
|
||||
// The standing ruling on this wall is that a label too long for its box WRAPS; nowrap forced
|
||||
// the browser to clip instead. Wrapping costs a second line in the one corner that needs it.
|
||||
'.pfs > .pbtn{flex:0 1 auto;min-width:0;white-space:normal;overflow-wrap:break-word;' +
|
||||
'text-align:center;line-height:1.2}' +
|
||||
'.is-dark .pfs{background:#132019;border-color:rgba(110,231,183,.24)}' +
|
||||
// ── THE REVEALED SIZE ROW ────────────────────────────────────────────────────
|
||||
// Sits directly under the print row, still hanging into the grid's 88px row-gap.
|
||||
// ⚠ THE OFFSET IS MEASURED, NOT GUESSED. Both rows share .main as containing block, so
|
||||
// "under .pfs" means top:100% + .pfs's own height — and that height is NOT a constant:
|
||||
// the row wraps to a second line in long-label locales (ro/ru measured at 2 lines).
|
||||
// A hard-coded 20px would tuck the size row under the print row in exactly those
|
||||
// locales. toggleDeck() reads the live offsetHeight and writes it to --pstocks-gap.
|
||||
// Hangs below the footer sticker, which is itself half-overhanging the banner edge — so the
|
||||
// offset starts at HALF the print row's height, not all of it.
|
||||
'.pstocks{top:100%;margin-top:calc(var(--pstocks-gap,20px) / 2);width:92%;max-width:92%;' +
|
||||
'box-sizing:border-box;padding:3px;border-radius:2px;background:#fff;' +
|
||||
'display:flex;flex-wrap:wrap;gap:3px;justify-content:center;' +
|
||||
'box-shadow:0 1px 4px rgba(0,0,0,.10);cursor:default}' +
|
||||
'.is-dark .pstocks{background:#132019;border-color:rgba(110,231,183,.24)}' +
|
||||
'.pfs > legend{margin:0 auto;padding:0 5px;font-size:8px;text-transform:uppercase;' +
|
||||
'letter-spacing:.6px;font-weight:700;color:#8a93a0}' +
|
||||
'.is-dark .pfs > legend{color:#7f9c8c}' +
|
||||
|
|
@ -616,13 +733,27 @@ useShadowStyles(
|
|||
'border-radius:2px;background:transparent;transition:background .15s}' +
|
||||
'.pbtn:hover{color:#10b981}' +
|
||||
'.pbtn:hover::after{background:#10b981}' +
|
||||
// Stock picker (cardStocks). Sized a shade smaller than .pbtn so it reads as a
|
||||
// SETTING for the neighbouring Card Deck button rather than a fourth action, and
|
||||
// `flex:0 1 auto` keeps it from taking an equal share of the row the way .pbtn does.
|
||||
'.pstock{flex:0 1 auto;max-width:100%;border:1px solid #d8dde3;border-radius:3px;' +
|
||||
// The deck button is a DISCLOSURE, so it says so while open — a caret that flips,
|
||||
// plus the green the row already uses for "active", so the reader can see which
|
||||
// card's sizes are showing without reading aria-expanded.
|
||||
'.pbtn-deck[aria-expanded="true"]{color:#10b981}' +
|
||||
'.pbtn-deck::before{content:"\\25B8";display:inline-block;margin-right:3px;font-size:8px;' +
|
||||
'transition:transform .15s}' +
|
||||
'.pbtn-deck[aria-expanded="true"]::before{transform:rotate(90deg)}' +
|
||||
// Size choices. These are BUTTONS, not <option>s — the Driver ruled out a native
|
||||
// dropdown here because it collides with the other dropdowns on the host page.
|
||||
// Each one both picks the size and prints, so it is styled as an action.
|
||||
// ⚠ NO nowrap HERE, and this is a MEASURED correction rather than a preference. With
|
||||
// white-space:nowrap the two landscape labels rendered 161px inside a 169px row and CLIPPED
|
||||
// ("Card 3.5 x 2.5 in (landscape)", "Postcard 6 x 4 in (landscape)"). The standing ruling on
|
||||
// this wall is that a label too long for its box WRAPS — never an ellipsis, never a crop. So
|
||||
// each size takes a full row of its own when it needs one; flex-wrap on .pstocks gives it the
|
||||
// line, and min-width:0 lets the text actually break instead of forcing the box open.
|
||||
'.pstock{flex:0 1 auto;min-width:0;max-width:100%;border:1px solid #d8dde3;border-radius:3px;' +
|
||||
'background:#fff;color:#5b6672;font:inherit;font-size:9.5px;font-weight:600;' +
|
||||
'padding:2px 4px;cursor:pointer;line-height:1.25}' +
|
||||
'padding:2px 5px;cursor:pointer;line-height:1.25;white-space:normal;overflow-wrap:break-word}' +
|
||||
'.pstock:hover{border-color:#10b981;color:#10b981}' +
|
||||
'.pstock.is-on{border-color:#10b981;color:#0b7a5b;background:rgba(16,185,129,.10)}' +
|
||||
// previewAction='print' only. The zoom-in cursor says "this opens something", and
|
||||
// the focus-visible outline is not optional: it is how a keyboard user sees where
|
||||
// they are on a control that is otherwise just a picture.
|
||||
|
|
@ -665,6 +796,31 @@ useShadowStyles(
|
|||
'box-shadow:0 6px 10px -8px rgba(0,0,0,.35);border-radius:4px 4px 0 0}' +
|
||||
'.is-dark .sec-head{border-bottom-color:rgba(255,255,255,.12);background:#0a1410;' +
|
||||
'box-shadow:0 6px 10px -8px rgba(0,0,0,.7)}' +
|
||||
// ★ THE TRANSPARENT STRIP ABOVE A PINNED HEADER (Driver 2026-09-25): "there is a transparent
|
||||
// section above the header, so when you're scrolling the posters are visible above the header
|
||||
// ... in the white space between the header and the top of the page."
|
||||
//
|
||||
// CAUSE, MEASURED at 1440px: `position:sticky; top:0` pins to the scroll container's PADDING
|
||||
// BOX, not its border box. .wall has padding-top:22px, so a pinned header comes to rest 22px
|
||||
// down and the cards keep scrolling through those 22px. Probed at scrollTop 400/900/1600 the
|
||||
// element painting in that strip was IMG.img every time — literally posters flying past above
|
||||
// the header, exactly as reported.
|
||||
//
|
||||
// WHY A PSEUDO-ELEMENT AND NOT `top:-22px`. Pulling the header up by the padding does pin it
|
||||
// flush, but sticky clips nothing: the header's own first 22px would then sit outside the
|
||||
// container and its text would be cut off. And padding-top on the header itself would move the
|
||||
// text down instead of covering anything. A ::before that extends the SAME opaque fill upward
|
||||
// covers the strip while leaving both layout and the header's own position untouched — the
|
||||
// `outline` vs `border` distinction again: paint outside the box, do not re-lay-out.
|
||||
//
|
||||
// Height comes from --wall-pad-top (set on .wall), never a second literal 22px.
|
||||
// Full-bleed on x: the header is inset 2px by its own margin and the strip has to cover the
|
||||
// container's 18px side padding too, or the posters show through at the edges.
|
||||
'.sec-head::before{content:"";position:absolute;left:-20px;right:-20px;' +
|
||||
'bottom:100%;height:var(--wall-pad-top,0px);background:inherit;pointer-events:none}' +
|
||||
// Nothing to cover when the wall is not the scroller (--wall-pad-top is 0px there), but be
|
||||
// explicit: an embedded wall must never paint a bar over its host.
|
||||
'.wall--embedded .sec-head::before{display:none}' +
|
||||
// A collapsed row is a plain bar — no bottom rule reaching for content that is
|
||||
// not there.
|
||||
'.sec-head.is-shut{border-bottom-color:transparent;margin-bottom:6px}' +
|
||||
|
|
@ -684,7 +840,51 @@ useShadowStyles(
|
|||
'.sec-count{font-size:11px;opacity:.6;font-variant-numeric:tabular-nums}' +
|
||||
// Poster aspect is 3ft x 3ft SQUARE — the real print ratio, so the wall previews
|
||||
// the actual artefact rather than an arbitrary box.
|
||||
'.main{position:relative;aspect-ratio:1/1;border:1px solid #dfe3e7;overflow:hidden;' +
|
||||
// ★ THE BANNER IS NOT THE MAP — and until now it effectively was.
|
||||
// Driver 2026-09-25, the sticker/olive-oil-bottle brief: this box is a BANNER that DISPLAYS
|
||||
// the map, the way a label displays a design. A banner taped to a wall gets a sticker at the
|
||||
// top and one at the bottom, each straddling its edge — "slight overlap over the banner, but
|
||||
// not over the content of the banner... if I put a piece of tape over the text it would be
|
||||
// offensive to the map."
|
||||
//
|
||||
// MEASURED before this change, and it is the whole problem: banner 240-426, map 241-425.
|
||||
// whiteSpaceTopOfBanner 1px, whiteSpaceBottomOfBanner 1px. The map filled the banner edge to
|
||||
// edge, so there was NO banner surface for a sticker to sit on — the bottom sticker could
|
||||
// only ever hang off into space (measured straddle: 1px on, 67px off) or cover the map.
|
||||
// Both wrong, and no amount of moving the sticker fixes it; the BANNER had to grow.
|
||||
//
|
||||
// --banner-pad is that room. .img is object-fit:contain, which fits the CONTENT box, so
|
||||
// padding reserves banner surface and the map shrinks to sit inside it rather than spilling.
|
||||
// The bottom gets MORE than the top because the footer sticker is taller than the header one
|
||||
// and, per the brief, should overhang the page BELOW the banner more than it covers the
|
||||
// banner itself ("probably an equal or more overlap").
|
||||
//
|
||||
// ⚠ aspect-ratio applies to the BORDER box here (box-sizing is border-box in this root), so
|
||||
// the square is the BANNER's shape, not the map's. The map inside is correspondingly shorter
|
||||
// than it is wide, which is correct: the banner is the thing being framed.
|
||||
//
|
||||
// overflow IS VISIBLE, DELIBERATELY. .pfs lives INSIDE this banner so its top:100% resolves
|
||||
// here; under `hidden` the print row was clipped to nothing. Nothing else needs the clip:
|
||||
// .img is object-fit:contain (never paints past its box) and .ph is a smaller flow box.
|
||||
// ⚠ DERIVED FROM THE STICKER HEIGHTS, NOT CHOSEN — and derived with SLACK, because the
|
||||
// heights are fractional. A sticker centred on the banner edge puts HALF its height onto the
|
||||
// banner, so the reserved white space must exceed that half.
|
||||
// attempt 1: 10px/26px by eye vs 34px/68px stickers -> map covered by 6px and 8px.
|
||||
// attempt 2: exactly half + 4px (21px/38px) -> STILL 1px of overlap on 27 cards in en and
|
||||
// ro but NOT zh, where the print row is 54px rather than 68px. The rects are
|
||||
// fractional (68.x), so half of them rounds past an integer constant; and the
|
||||
// row's height is LOCALE-DEPENDENT, which no fixed number can track.
|
||||
// Hence generous slack rather than a tighter constant: 12px past the widest measured half.
|
||||
// The gate asserts the relationship (map-covered 0), so if a future locale wraps the row to
|
||||
// three lines this fails loudly instead of quietly shaving the map.
|
||||
// Now that the OVERLAP is a constant (--tape-overlap on .card) rather than half a
|
||||
// content-dependent height, the banner only has to reserve that overlap plus daylight —
|
||||
// the same number in every locale at every width. It was 29/50 chasing half of a row that
|
||||
// is 68px in en, 54px in zh and taller still when ro wraps at 390px; that chase is what put
|
||||
// 4px of sticker on the map. 16 + 8 of daylight, both ends.
|
||||
'.main{--banner-pad-top:24px;--banner-pad-bottom:24px;' +
|
||||
'position:relative;aspect-ratio:1/1;border:1px solid #dfe3e7;overflow:visible;' +
|
||||
'padding:var(--banner-pad-top) 10px var(--banner-pad-bottom);' +
|
||||
'background:#fbfcfd;display:flex;align-items:center;justify-content:center}' +
|
||||
'.is-dark .main{background:#0c1712;border-color:rgba(255,255,255,.08)}' +
|
||||
// CONTAIN, not cover: `cover` crops to fill, which on a square sheet sliced off
|
||||
|
|
@ -798,26 +998,44 @@ useShadowStyles(
|
|||
the two beside it. -->
|
||||
<div class="ph-note">preview unavailable</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="tape pfs">
|
||||
<legend>{{ t('buttons.printOptions') }}</legend>
|
||||
<button v-if="!(hidePrayerListWhenSoon && !canPrintRows)" type="button" class="pbtn"
|
||||
@click="downloadPacket(item)">{{ t('buttons.prayerList') }}</button>
|
||||
<!-- CARD DECK — only when this wall can supply rows. A button that
|
||||
cannot work is worse than an absent one, so it is gated on the
|
||||
records provider rather than shown and then failing at click. -->
|
||||
<button v-if="canPrintRows" type="button" class="pbtn"
|
||||
@click="prints.printCardDeck(item)">{{ t('buttons.cardDeck') }}</button>
|
||||
<!-- Stock picker, only where the wall offers one AND the deck is printable.
|
||||
@click.stop so choosing a size does not also fire the card's own
|
||||
show-on-map handler. Labels come from card-render.js STOCK. -->
|
||||
<select v-if="canPrintRows && stockOptions.length" class="pstock"
|
||||
v-model="pickedStock" :aria-label="t('buttons.cardStockAria')" @click.stop>
|
||||
<option v-for="s in stockOptions" :key="s.id" :value="s.id">{{ s.label }}</option>
|
||||
</select>
|
||||
<button type="button" class="pbtn" @click="exportPoster(item)">{{ t('buttons.posterMap') }}</button>
|
||||
</fieldset>
|
||||
<!-- ★ THE PRINT ROW LIVES INSIDE .main, NOT BESIDE IT (Driver 2026-09-25).
|
||||
It is `position:absolute; top:100%`, which resolves against the nearest
|
||||
positioned ANCESTOR. As a SIBLING of .main that ancestor was .card, whose
|
||||
bottom sits 26px past the frame — measured: frame bottom 426, row top 452.
|
||||
That 26px of nothing is why it read as a detached row instead of tape.
|
||||
Moved INSIDE .main, top:100% is the frame's own bottom edge, so the row
|
||||
starts exactly on the border it is taping over. -->
|
||||
<fieldset class="tape pfs">
|
||||
<legend>{{ t('buttons.printOptions') }}</legend>
|
||||
<button v-if="!(hidePrayerListWhenSoon && !canPrintRows)" type="button" class="pbtn"
|
||||
@click="downloadPacket(item)">{{ t('buttons.prayerList') }}</button>
|
||||
<button type="button" class="pbtn" @click="exportPoster(item)">{{ t('buttons.posterMap') }}</button>
|
||||
<!-- ★ CARD DECK IS NOT A PEER OPTION, AND ITS SIZE PICKER IS HIDDEN UNTIL ASKED
|
||||
FOR (Driver 2026-09-25): "card should not be second option, and I don't want
|
||||
to add the dropdown for the card because it makes it hard for others to have
|
||||
dropdowns too... only let you click on the dropdown when you click on print
|
||||
the cards, which will show you a hidden dropdown to let you choose the print
|
||||
size option you want."
|
||||
So the deck is a DISCLOSURE, not a sibling button: clicking it reveals this
|
||||
card's own size row, and the size buttons there are what actually print.
|
||||
A 162px <select> was the single widest control in a 172px row — removing it
|
||||
from the default row is most of why the row now fits on one line. -->
|
||||
<button v-if="canPrintRows" type="button" class="pbtn pbtn-deck"
|
||||
:aria-expanded="String(deckOpenFor === item.id)"
|
||||
@click.stop="toggleDeck(item.id, $event)">{{ t('buttons.cardDeck') }}</button>
|
||||
</fieldset>
|
||||
|
||||
<!-- The revealed size row. Rendered as SIBLINGS in a second tape rather than a
|
||||
<select> so there is no native dropdown to collide with other dropdowns —
|
||||
which is the constraint the Driver gave, not a styling preference. -->
|
||||
<div v-if="canPrintRows && stockOptions.length && deckOpenFor === item.id"
|
||||
class="tape pstocks" role="group" :aria-label="t('buttons.cardStockAria')">
|
||||
<button v-for="s in stockOptions" :key="s.id" type="button" class="pstock"
|
||||
:class="{ 'is-on': pickedStock === s.id }"
|
||||
@click.stop="printDeckAt(item, s.id)">{{ s.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -76,23 +76,74 @@ export function usePosterPrints(opts = {}) {
|
|||
// ── 1. THE POSTER ─────────────────────────────────────────────────────────
|
||||
// Prefers the pre-rendered image; falls back to rasterising the poster's own
|
||||
// HTML in the browser, which is what lets the repo stop shipping 41 MB of webp.
|
||||
// ★ THIS IS THE WALL'S posterAction='print' PATH, PROMOTED. It was a private function in
|
||||
// PosterWall.vue, which meant the MAP LEGEND's own "print this list's poster" had no way to
|
||||
// reach it — and the legend had grown a fourth implementation instead: a hand-rolled
|
||||
// `<a download>` of the .webp. That was wrong twice over. It silently downloads a file,
|
||||
// which the Driver ruled out explicitly (2026-09-23: "no silent file download as the default
|
||||
// action"), and it has NO fallback, so on the 27 lists whose .webp does not ship it simply
|
||||
// saved a 404.
|
||||
//
|
||||
// Two guards below were each learned the hard way and MUST survive any future move:
|
||||
// 1. a THROWN fetch is a MISS, not an error (blocked/offline/refused reject rather than
|
||||
// returning !ok). Letting it escape skips the rasterise fallback entirely — measured
|
||||
// during the wall migration, when check-poster-print-fallback went PASS -> FAIL with 0
|
||||
// .html requests.
|
||||
// 2. a blank raster must fail LOUDLY. An SVG inside an <img> is an isolated document, so a
|
||||
// missing stylesheet paints every fill black with no error at all.
|
||||
//
|
||||
// `assetsBase` is the seam: the wall and the map profile resolve their own base, and this
|
||||
// stays ignorant of where a bundle serves its posters from.
|
||||
async function printPoster(item) {
|
||||
if (busy()) return;
|
||||
const kind = 'Poster';
|
||||
const base = opts.assetsBase ? opts.assetsBase() : null;
|
||||
const id = item?.id ?? item;
|
||||
set(item, kind, 'fetch', 'fetching the poster', 25);
|
||||
try {
|
||||
const url = opts.posterUrl ? opts.posterUrl(item) : null;
|
||||
let html = '';
|
||||
if (url) {
|
||||
const res = await fetch(url, { cache: 'force-cache' });
|
||||
if (res.ok) html = await res.text();
|
||||
}
|
||||
if (!html) throw new Error('no poster available for this item');
|
||||
openPrintFrame(html, { autoprint: true });
|
||||
done(item, kind);
|
||||
// An explicit posterUrl still wins — it is how a caller points at a poster that is not
|
||||
// <base>/posters/<id>.webp at all.
|
||||
const direct = opts.posterUrl ? opts.posterUrl(item) : null;
|
||||
let res = null;
|
||||
const webp = direct || (base ? `${base}/posters/${id}.webp` : null);
|
||||
if (webp) { try { res = await fetch(webp); } catch { /* a miss, see guard 1 */ } }
|
||||
if (res && res.ok) { await deliverPoster(await res.blob(), item, kind); return; }
|
||||
if (!base) throw new Error('no poster available for this item');
|
||||
// NO PRE-RENDERED IMAGE? GENERATE IT. The .webp posters are ~41MB across a tree and are
|
||||
// not shipped by default; the HTML is the SOURCE and is smaller than the image it makes.
|
||||
set(item, kind, 'render', 'generating the poster', 45);
|
||||
const htmlRes = await fetch(`${base}/posters/${id}.html`);
|
||||
if (!htmlRes.ok) throw new Error(`poster not found (${htmlRes.status})`);
|
||||
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js');
|
||||
const blob = await rasterPoster(await htmlRes.text(), { scale: 2 });
|
||||
if (!(await looksRendered(blob))) throw new Error('the generated poster rendered blank');
|
||||
await deliverPoster(blob, item, kind);
|
||||
} catch (e) { fail(item, kind, e); }
|
||||
}
|
||||
|
||||
// A 3ft SQUARE on a PORTRAIT sheet must fit by its SHORTER axis: scaling to the full page
|
||||
// WIDTH makes it taller than the printable height and it overflows onto a second page.
|
||||
// Constrain both axes and let object-fit letterbox it.
|
||||
const posterObjectUrls = [];
|
||||
async function deliverPoster(blob, item, kind) {
|
||||
set(item, kind, 'save', 'opening the print dialog', 80);
|
||||
const url = URL.createObjectURL(blob);
|
||||
posterObjectUrls.push(url);
|
||||
const title = `${opts.downloadPrefix || 'doxa'} — ${label(item)}`;
|
||||
openPrintFrame(
|
||||
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>' + esc(title) + '</title>'
|
||||
+ '<style>@page{size:letter portrait;margin:12mm}'
|
||||
+ 'html,body{margin:0;height:100%;background:#fff}'
|
||||
+ 'body{display:flex;align-items:center;justify-content:center}'
|
||||
+ 'img{max-width:100%;max-height:100vh;object-fit:contain;display:block}'
|
||||
+ '@media print{body{height:auto}'
|
||||
+ 'img{width:auto;max-width:100%;max-height:100%;object-fit:contain}}</style></head>'
|
||||
+ `<body><img src="${url}" alt="${esc(label(item))}"></body></html>`,
|
||||
{ autoprint: true },
|
||||
);
|
||||
done(item, kind);
|
||||
}
|
||||
|
||||
// ── 2. THE LIST ───────────────────────────────────────────────────────────
|
||||
// The Driver's FIRST option — "that's what the whole app was about": the list
|
||||
// itself as a print-paginated table with a prayer checkbox per row.
|
||||
|
|
|
|||
25
library/i18n/locales/ar/connectivity.json
Normal file
25
library/i18n/locales/ar/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "الوصول إلى الإنترنت حيث تعيش الشعوب غير المنخرطة",
|
||||
"bands": {
|
||||
"yes": "متصل 60%+",
|
||||
"partial": "جزئي 25–59%",
|
||||
"no": "منخفض، أقل من 25%",
|
||||
"unknown": "غير مُقاس"
|
||||
},
|
||||
"peopleGroup": "شعب",
|
||||
"caveat": "أرقام وطنية (البنك الدولي). بيانات الاتصال لكل مدينة غير متاحة للعامة — لا تستنتج حال مدينة من بيانات بلدها.",
|
||||
"sheet": {
|
||||
"close": "إغلاق",
|
||||
"ai": "عُثر على هذه البيانات عبر بحث معمّق بالذكاء الاصطناعي — وقد تتضمن معلومات خاطئة.",
|
||||
"dismiss": "إغلاق التنبيه",
|
||||
"internetUsers": "مستخدمو الإنترنت",
|
||||
"measured": "سنة القياس",
|
||||
"mobilePer100": "اشتراكات الجوال لكل 100",
|
||||
"peopleGroups": "الشعوب",
|
||||
"topPlatform": "المنصة الأولى",
|
||||
"unknown": "غير معروف",
|
||||
"carriers": "شركات الاتصالات ومزودو الإنترنت ({n})",
|
||||
"socialPlatforms": "منصات التواصل الاجتماعي",
|
||||
"note": "أرقام وطنية وليست قياسات للمدن. أسماء شركات الاتصالات هي أسماء أصحاب سجلات ASN — وتشمل البنوك والجامعات إلى جانب مزودي الإنترنت، لذا فإن تصنيفها كشركات اتصالات تقدير مبني على الاسم وليس تصنيفًا مُتحقَّقًا منه."
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "القائمة قادمة قريباً",
|
||||
"pinHint": {
|
||||
"tapDot": "اضغط على نقطة ملوّنة — لا يزال هناك من يحتاج إلى الصلاة"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "قائمتي UPG 100 — قوائم الصلاة والملصقات",
|
||||
"upg100WallTag": "تعبئة للصلاة",
|
||||
"upg100WallIntro": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم <strong>باسم شعبهم</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "الدولة",
|
||||
"upg100Region": "المنطقة",
|
||||
"upg100Zone": "قوائم UPG-100",
|
||||
"upg100Group": "مجموعة شعبية"
|
||||
}
|
||||
"upg100Group": "مجموعة شعبية",
|
||||
"selectView": "اختر العرض",
|
||||
"views": "العروض",
|
||||
"prayerResources": "موارد الصلاة",
|
||||
"map": "الخريطة"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/de/connectivity.json
Normal file
25
library/i18n/locales/de/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "Internetzugang dort, wo Volksgruppen ohne Engagement leben",
|
||||
"bands": {
|
||||
"yes": "verbunden 60%+",
|
||||
"partial": "teilweise 25–59%",
|
||||
"no": "gering, unter 25%",
|
||||
"unknown": "nicht gemessen"
|
||||
},
|
||||
"peopleGroup": "Volksgruppe",
|
||||
"caveat": "LANDESWEITE Zahlen (Weltbank). Konnektivität je Stadt ist nicht öffentlich verfügbar — schließen Sie nicht vom Land auf eine Stadt.",
|
||||
"sheet": {
|
||||
"close": "Schließen",
|
||||
"ai": "Diese Daten wurden durch KI-gestützte Recherche gefunden — sie können falsche Angaben enthalten.",
|
||||
"dismiss": "Ausblenden",
|
||||
"internetUsers": "Internetnutzer",
|
||||
"measured": "Gemessen",
|
||||
"mobilePer100": "Mobilfunk pro 100",
|
||||
"peopleGroups": "Volksgruppen",
|
||||
"topPlatform": "Wichtigste Plattform",
|
||||
"unknown": "unbekannt",
|
||||
"carriers": "Netzbetreiber & ISPs ({n})",
|
||||
"socialPlatforms": "Soziale Plattformen",
|
||||
"note": "Landesweite Zahlen, keine Messungen je Stadt. Betreibernamen sind ASN-Registerinhaber — darunter neben ISPs auch Banken und Universitäten; die Einstufung als Netzbetreiber ist daher eine Namensheuristik, keine geprüfte Klassifizierung."
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menü folgt in Kürze",
|
||||
"pinHint": {
|
||||
"tapDot": "Tippe auf einen farbigen Punkt — jemand braucht noch Gebet"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "Meine UPG-100-Liste — Gebetslisten und Poster",
|
||||
"upg100WallTag": "Gebetsmobilisierung",
|
||||
"upg100WallIntro": "Teilen Sie die Gebetsaufgabe in Gruppen zu fünfzig und hundert auf — so wie Jesus die 5.000 speiste (Lukas 9,14). Jedes Poster ist eine 100er-Liste, die Sie beten, drucken und an die Wand hängen können — und für sie <strong>mit dem Namen ihrer Volksgruppe</strong> beten."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Land",
|
||||
"upg100Region": "Region",
|
||||
"upg100Zone": "UPG-100-Listen",
|
||||
"upg100Group": "Volksgruppe"
|
||||
}
|
||||
"upg100Group": "Volksgruppe",
|
||||
"selectView": "Ansicht wählen",
|
||||
"views": "Ansichten",
|
||||
"prayerResources": "Gebetsressourcen",
|
||||
"map": "Karte"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/en/connectivity.json
Normal file
25
library/i18n/locales/en/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "Internet access where unengaged people groups live",
|
||||
"bands": {
|
||||
"yes": "connected 60%+",
|
||||
"partial": "partial 25–59%",
|
||||
"no": "low, under 25%",
|
||||
"unknown": "not measured"
|
||||
},
|
||||
"peopleGroup": "people group",
|
||||
"caveat": "NATIONAL figures (World Bank). Per-city connectivity is not publicly available — do not read a city off a country.",
|
||||
"sheet": {
|
||||
"close": "Close",
|
||||
"ai": "Data was found by AI deep research — it is subject to false information.",
|
||||
"dismiss": "Dismiss",
|
||||
"internetUsers": "Internet users",
|
||||
"measured": "Measured",
|
||||
"mobilePer100": "Mobile per 100",
|
||||
"peopleGroups": "People groups",
|
||||
"topPlatform": "Top platform",
|
||||
"unknown": "unknown",
|
||||
"carriers": "Carriers & ISPs ({n})",
|
||||
"socialPlatforms": "Social platforms",
|
||||
"note": "National figures, not city measurements. Carrier names are ASN registry holders — that set includes banks and universities as well as ISPs, so the carrier flag is a name heuristic, not a verified classification."
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menu coming soon",
|
||||
"pinHint": {
|
||||
"tapDot": "Tap a coloured dot — someone still needs prayer"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "My UPG 100 List — Prayer Lists & Posters",
|
||||
"upg100WallTag": "Prayer Mobilization",
|
||||
"upg100WallIntro": "Hand away the prayer task in clusters of fifties and hundreds — the way Jesus fed the 5,000 (Luke 9:14). Each poster is a 100-list you can pray, print, and put on a wall — praying for them <strong>by their people group name</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Country",
|
||||
"upg100Region": "Region",
|
||||
"upg100Zone": "UPG-100 Lists",
|
||||
"upg100Group": "People Group"
|
||||
}
|
||||
"upg100Group": "People Group",
|
||||
"selectView": "Select View",
|
||||
"views": "Views",
|
||||
"prayerResources": "Prayer Resources",
|
||||
"map": "Map"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/es/connectivity.json
Normal file
25
library/i18n/locales/es/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "Acceso a internet donde viven pueblos sin implicación",
|
||||
"bands": {
|
||||
"yes": "conectado 60%+",
|
||||
"partial": "parcial 25–59%",
|
||||
"no": "bajo, menos del 25%",
|
||||
"unknown": "sin medir"
|
||||
},
|
||||
"peopleGroup": "pueblo",
|
||||
"caveat": "Cifras NACIONALES (Banco Mundial). La conectividad por ciudad no está disponible públicamente: no deduzca una ciudad a partir de su país.",
|
||||
"sheet": {
|
||||
"close": "Cerrar",
|
||||
"ai": "Estos datos se obtuvieron mediante investigación con IA y pueden contener información falsa.",
|
||||
"dismiss": "Descartar",
|
||||
"internetUsers": "Usuarios de internet",
|
||||
"measured": "Medido",
|
||||
"mobilePer100": "Móviles por cada 100",
|
||||
"peopleGroups": "Pueblos",
|
||||
"topPlatform": "Plataforma principal",
|
||||
"unknown": "desconocido",
|
||||
"carriers": "Operadores y proveedores de internet ({n})",
|
||||
"socialPlatforms": "Redes sociales",
|
||||
"note": "Cifras nacionales, no mediciones por ciudad. Los nombres de operadores son titulares de registros ASN (incluyen bancos y universidades además de proveedores), así que la marca de operador es una heurística por nombre, no una clasificación verificada."
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menú próximamente",
|
||||
"pinHint": {
|
||||
"tapDot": "Toca un punto de color: alguien todavía necesita oración"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "Mi lista UPG 100 — Listas de oración y carteles",
|
||||
"upg100WallTag": "Movilización de oración",
|
||||
"upg100WallIntro": "Reparte la tarea de la oración en grupos de cincuenta y de cien, como Jesús alimentó a los 5.000 (Lucas 9:14). Cada cartel es una lista de 100 que puedes orar, imprimir y poner en la pared — orando por ellos <strong>por el nombre de su pueblo</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "País",
|
||||
"upg100Region": "Región",
|
||||
"upg100Zone": "Listas UPG-100",
|
||||
"upg100Group": "Grupo étnico"
|
||||
}
|
||||
"upg100Group": "Grupo étnico",
|
||||
"selectView": "Seleccionar vista",
|
||||
"views": "Vistas",
|
||||
"prayerResources": "Recursos de oración",
|
||||
"map": "Mapa"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/fr/connectivity.json
Normal file
25
library/i18n/locales/fr/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "Accès à Internet là où vivent les peuples sans engagement",
|
||||
"bands": {
|
||||
"yes": "connecté 60 %+",
|
||||
"partial": "partiel 25–59 %",
|
||||
"no": "faible, moins de 25 %",
|
||||
"unknown": "non mesuré"
|
||||
},
|
||||
"peopleGroup": "peuple",
|
||||
"caveat": "Chiffres NATIONAUX (Banque mondiale). La connectivité par ville n'est pas publique — ne déduisez pas une ville à partir de son pays.",
|
||||
"sheet": {
|
||||
"close": "Fermer",
|
||||
"ai": "Ces données proviennent d'une recherche approfondie par IA — elles peuvent contenir des informations erronées.",
|
||||
"dismiss": "Ignorer",
|
||||
"internetUsers": "Internautes",
|
||||
"measured": "Mesuré",
|
||||
"mobilePer100": "Mobiles pour 100",
|
||||
"peopleGroups": "Peuples",
|
||||
"topPlatform": "Plateforme principale",
|
||||
"unknown": "inconnu",
|
||||
"carriers": "Opérateurs et FAI ({n})",
|
||||
"socialPlatforms": "Réseaux sociaux",
|
||||
"note": "Chiffres nationaux, pas des mesures par ville. Les noms d'opérateurs sont les titulaires de registres ASN (on y trouve aussi des banques et des universités à côté des FAI), donc l'étiquette « opérateur » est une heuristique fondée sur le nom, non une classification vérifiée."
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menu bientôt disponible",
|
||||
"pinHint": {
|
||||
"tapDot": "Touchez un point coloré — quelqu’un a encore besoin de prière"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "Ma liste UPG 100 — Listes de prière et affiches",
|
||||
"upg100WallTag": "Mobilisation pour la prière",
|
||||
"upg100WallIntro": "Répartissez la tâche de la prière par groupes de cinquante et de cent — comme Jésus a nourri les 5 000 (Luc 9:14). Chaque affiche est une liste de 100 que vous pouvez prier, imprimer et afficher au mur — en priant pour eux <strong>par le nom de leur peuple</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Pays",
|
||||
"upg100Region": "Région",
|
||||
"upg100Zone": "Listes UPG-100",
|
||||
"upg100Group": "Groupe de peuples"
|
||||
}
|
||||
"upg100Group": "Groupe de peuples",
|
||||
"selectView": "Choisir la vue",
|
||||
"views": "Vues",
|
||||
"prayerResources": "Ressources de prière",
|
||||
"map": "Carte"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/hi/connectivity.json
Normal file
25
library/i18n/locales/hi/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "इंटरनेट पहुँच जहाँ बिना सहभागिता वाले जनसमूह रहते हैं",
|
||||
"bands": {
|
||||
"yes": "जुड़ा हुआ 60%+",
|
||||
"partial": "आंशिक 25–59%",
|
||||
"no": "कम, 25% से नीचे",
|
||||
"unknown": "मापा नहीं गया"
|
||||
},
|
||||
"peopleGroup": "जनसमूह",
|
||||
"caveat": "राष्ट्रीय आँकड़े (विश्व बैंक)। शहर-स्तर की कनेक्टिविटी सार्वजनिक रूप से उपलब्ध नहीं है — देश के आँकड़ों से किसी शहर का अनुमान न लगाएँ।",
|
||||
"sheet": {
|
||||
"close": "बंद करें",
|
||||
"ai": "यह डेटा एआई गहन शोध से मिला है — इसमें गलत जानकारी हो सकती है।",
|
||||
"dismiss": "हटाएँ",
|
||||
"internetUsers": "इंटरनेट उपयोगकर्ता",
|
||||
"measured": "मापा गया",
|
||||
"mobilePer100": "प्रति 100 मोबाइल",
|
||||
"peopleGroups": "जनसमूह",
|
||||
"topPlatform": "शीर्ष प्लेटफ़ॉर्म",
|
||||
"unknown": "अज्ञात",
|
||||
"carriers": "कैरियर और आईएसपी ({n})",
|
||||
"socialPlatforms": "सोशल प्लेटफ़ॉर्म",
|
||||
"note": "ये राष्ट्रीय आँकड़े हैं, शहरों के माप नहीं। कैरियर के नाम ASN रजिस्ट्री धारकों के हैं — उनमें आईएसपी के साथ बैंक और विश्वविद्यालय भी शामिल हैं, इसलिए कैरियर का चिह्न नाम पर आधारित अनुमान है, सत्यापित वर्गीकरण नहीं।"
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "मेनू शीघ्र आ रहा है",
|
||||
"pinHint": {
|
||||
"tapDot": "किसी रंगीन बिंदु पर टैप करें — किसी को अभी भी प्रार्थना की आवश्यकता है"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "मेरी UPG 100 सूची — प्रार्थना सूचियाँ और पोस्टर",
|
||||
"upg100WallTag": "प्रार्थना लामबंदी",
|
||||
"upg100WallIntro": "प्रार्थना के काम को पचास-पचास और सौ-सौ के समूहों में बाँटें — जैसे यीशु ने 5,000 लोगों को खिलाया (लूका 9:14)। हर पोस्टर 100 की एक सूची है जिसके लिए आप प्रार्थना कर सकते हैं, छाप सकते हैं और दीवार पर लगा सकते हैं — उनके लिए <strong>उनके जनसमूह के नाम से</strong> प्रार्थना करते हुए।"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "देश",
|
||||
"upg100Region": "क्षेत्र",
|
||||
"upg100Zone": "UPG-100 सूचियाँ",
|
||||
"upg100Group": "जनसमूह"
|
||||
}
|
||||
"upg100Group": "जनसमूह",
|
||||
"selectView": "दृश्य चुनें",
|
||||
"views": "दृश्य",
|
||||
"prayerResources": "प्रार्थना संसाधन",
|
||||
"map": "मानचित्र"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/it/connectivity.json
Normal file
25
library/i18n/locales/it/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "Accesso a internet dove vivono i popoli senza impegno",
|
||||
"bands": {
|
||||
"yes": "connesso 60%+",
|
||||
"partial": "parziale 25–59%",
|
||||
"no": "basso, sotto il 25%",
|
||||
"unknown": "non misurato"
|
||||
},
|
||||
"peopleGroup": "popolo",
|
||||
"caveat": "Dati NAZIONALI (Banca Mondiale). La connettività per città non è disponibile pubblicamente: non dedurre una città dal suo paese.",
|
||||
"sheet": {
|
||||
"close": "Chiudi",
|
||||
"ai": "Questi dati provengono da una ricerca approfondita con IA e possono contenere informazioni errate.",
|
||||
"dismiss": "Ignora",
|
||||
"internetUsers": "Utenti internet",
|
||||
"measured": "Misurato",
|
||||
"mobilePer100": "Cellulari ogni 100",
|
||||
"peopleGroups": "Popoli",
|
||||
"topPlatform": "Piattaforma principale",
|
||||
"unknown": "sconosciuto",
|
||||
"carriers": "Operatori e ISP ({n})",
|
||||
"socialPlatforms": "Piattaforme social",
|
||||
"note": "Dati nazionali, non misure per città. I nomi degli operatori sono titolari di registri ASN (includono banche e università oltre agli ISP), quindi l'etichetta di operatore è un'euristica basata sul nome, non una classificazione verificata."
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menu in arrivo",
|
||||
"pinHint": {
|
||||
"tapDot": "Tocca un punto colorato — qualcuno ha ancora bisogno di preghiera"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "La mia lista UPG 100 — Liste di preghiera e poster",
|
||||
"upg100WallTag": "Mobilitazione alla preghiera",
|
||||
"upg100WallIntro": "Distribuisci il compito della preghiera in gruppi di cinquanta e di cento — come Gesù sfamò i 5.000 (Luca 9:14). Ogni poster è una lista di 100 che puoi pregare, stampare e appendere al muro — pregando per loro <strong>con il nome del loro popolo</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Paese",
|
||||
"upg100Region": "Regione",
|
||||
"upg100Zone": "Liste UPG-100",
|
||||
"upg100Group": "Gruppo etnico"
|
||||
}
|
||||
"upg100Group": "Gruppo etnico",
|
||||
"selectView": "Seleziona vista",
|
||||
"views": "Viste",
|
||||
"prayerResources": "Risorse di preghiera",
|
||||
"map": "Mappa"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/pt/connectivity.json
Normal file
25
library/i18n/locales/pt/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "Acesso à internet onde vivem os povos sem envolvimento",
|
||||
"bands": {
|
||||
"yes": "conectado 60%+",
|
||||
"partial": "parcial 25–59%",
|
||||
"no": "baixo, abaixo de 25%",
|
||||
"unknown": "não medido"
|
||||
},
|
||||
"peopleGroup": "povo",
|
||||
"caveat": "Números NACIONAIS (Banco Mundial). A conectividade por cidade não está disponível publicamente — não deduza uma cidade a partir do país.",
|
||||
"sheet": {
|
||||
"close": "Fechar",
|
||||
"ai": "Estes dados foram encontrados por pesquisa aprofundada com IA — podem conter informações falsas.",
|
||||
"dismiss": "Dispensar",
|
||||
"internetUsers": "Usuários de internet",
|
||||
"measured": "Medido",
|
||||
"mobilePer100": "Celulares por 100",
|
||||
"peopleGroups": "Povos",
|
||||
"topPlatform": "Plataforma principal",
|
||||
"unknown": "desconhecido",
|
||||
"carriers": "Operadoras e provedores ({n})",
|
||||
"socialPlatforms": "Redes sociais",
|
||||
"note": "Números nacionais, não medições por cidade. Os nomes das operadoras são titulares de registros ASN (incluem bancos e universidades além de provedores), então a marcação de operadora é uma heurística pelo nome, não uma classificação verificada."
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menu em breve",
|
||||
"pinHint": {
|
||||
"tapDot": "Toque num ponto colorido — alguém ainda precisa de oração"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "A minha lista UPG 100 — Listas de oração e cartazes",
|
||||
"upg100WallTag": "Mobilização de oração",
|
||||
"upg100WallIntro": "Reparta a tarefa da oração em grupos de cinquenta e de cem — como Jesus alimentou os 5.000 (Lucas 9:14). Cada cartaz é uma lista de 100 que pode orar, imprimir e pôr na parede — orando por eles <strong>pelo nome do seu povo</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "País",
|
||||
"upg100Region": "Região",
|
||||
"upg100Zone": "Listas UPG-100",
|
||||
"upg100Group": "Grupo étnico"
|
||||
}
|
||||
"upg100Group": "Grupo étnico",
|
||||
"selectView": "Selecionar vista",
|
||||
"views": "Vistas",
|
||||
"prayerResources": "Recursos de oração",
|
||||
"map": "Mapa"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/ro/connectivity.json
Normal file
25
library/i18n/locales/ro/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "Acces la internet unde trăiesc popoarele fără implicare",
|
||||
"bands": {
|
||||
"yes": "conectat 60%+",
|
||||
"partial": "parțial 25–59%",
|
||||
"no": "scăzut, sub 25%",
|
||||
"unknown": "nemăsurat"
|
||||
},
|
||||
"peopleGroup": "popor",
|
||||
"caveat": "Cifre NAȚIONALE (Banca Mondială). Conectivitatea pe oraș nu este disponibilă public — nu deduceți un oraș din datele țării.",
|
||||
"sheet": {
|
||||
"close": "Închide",
|
||||
"ai": "Aceste date au fost găsite prin cercetare aprofundată cu IA — pot conține informații false.",
|
||||
"dismiss": "Închide notificarea",
|
||||
"internetUsers": "Utilizatori de internet",
|
||||
"measured": "Măsurat",
|
||||
"mobilePer100": "Mobile la 100",
|
||||
"peopleGroups": "Popoare",
|
||||
"topPlatform": "Platforma principală",
|
||||
"unknown": "necunoscut",
|
||||
"carriers": "Operatori și ISP-uri ({n})",
|
||||
"socialPlatforms": "Platforme sociale",
|
||||
"note": "Cifre naționale, nu măsurători pe oraș. Numele operatorilor sunt titulari de înregistrări ASN (printre ei se află și bănci și universități, nu doar ISP-uri), deci eticheta de operator este o euristică după nume, nu o clasificare verificată."
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Meniu în curând",
|
||||
"pinHint": {
|
||||
"tapDot": "Atinge un punct colorat — cineva are încă nevoie de rugăciune"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "Lista mea UPG 100 — Liste de rugăciune și afișe",
|
||||
"upg100WallTag": "Mobilizare pentru rugăciune",
|
||||
"upg100WallIntro": "Împarte sarcina rugăciunii în grupuri de câte cincizeci și o sută — așa cum Isus a hrănit cei 5.000 (Luca 9:14). Fiecare afiș este o listă de 100 pe care o poți ruga, tipări și pune pe perete — rugându-te pentru ei <strong>pe numele poporului lor</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Țară",
|
||||
"upg100Region": "Regiune",
|
||||
"upg100Zone": "Liste UPG-100",
|
||||
"upg100Group": "Grup etnic"
|
||||
}
|
||||
"upg100Group": "Grup etnic",
|
||||
"selectView": "Alege vizualizarea",
|
||||
"views": "Vizualizări",
|
||||
"prayerResources": "Resurse de rugăciune",
|
||||
"map": "Hartă"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/ru/connectivity.json
Normal file
25
library/i18n/locales/ru/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "Доступ к интернету там, где живут народы без вовлечённости",
|
||||
"bands": {
|
||||
"yes": "подключено 60%+",
|
||||
"partial": "частично 25–59%",
|
||||
"no": "низкий, менее 25%",
|
||||
"unknown": "не измерено"
|
||||
},
|
||||
"peopleGroup": "народ",
|
||||
"caveat": "НАЦИОНАЛЬНЫЕ данные (Всемирный банк). Данных о подключении по городам в открытом доступе нет — не судите о городе по стране.",
|
||||
"sheet": {
|
||||
"close": "Закрыть",
|
||||
"ai": "Эти данные найдены с помощью углублённого поиска ИИ и могут содержать ошибки.",
|
||||
"dismiss": "Скрыть",
|
||||
"internetUsers": "Пользователи интернета",
|
||||
"measured": "Год измерения",
|
||||
"mobilePer100": "Мобильных на 100",
|
||||
"peopleGroups": "Народы",
|
||||
"topPlatform": "Главная платформа",
|
||||
"unknown": "неизвестно",
|
||||
"carriers": "Операторы и провайдеры ({n})",
|
||||
"socialPlatforms": "Социальные платформы",
|
||||
"note": "Национальные данные, а не измерения по городам. Названия операторов — это владельцы записей в реестре ASN; среди них есть банки и университеты, а не только провайдеры, поэтому пометка «оператор» — эвристика по названию, а не проверенная классификация."
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Меню скоро появится",
|
||||
"pinHint": {
|
||||
"tapDot": "Нажмите на цветную точку — кому-то всё ещё нужна молитва"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "Мой список UPG 100 — молитвенные списки и плакаты",
|
||||
"upg100WallTag": "Молитвенная мобилизация",
|
||||
"upg100WallIntro": "Разделите молитвенный труд на группы по пятьдесят и по сто — так Иисус накормил 5000 (Луки 9:14). Каждый плакат — это список из 100, о котором можно молиться, распечатать и повесить на стену, молясь о них <strong>по имени их народа</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Страна",
|
||||
"upg100Region": "Регион",
|
||||
"upg100Zone": "Списки UPG-100",
|
||||
"upg100Group": "Народ"
|
||||
}
|
||||
"upg100Group": "Народ",
|
||||
"selectView": "Выбрать вид",
|
||||
"views": "Виды",
|
||||
"prayerResources": "Молитвенные ресурсы",
|
||||
"map": "Карта"
|
||||
}
|
||||
|
|
|
|||
25
library/i18n/locales/zh/connectivity.json
Normal file
25
library/i18n/locales/zh/connectivity.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "未参与族群所在地的互联网接入",
|
||||
"bands": {
|
||||
"yes": "已联网 60%+",
|
||||
"partial": "部分 25–59%",
|
||||
"no": "低,低于 25%",
|
||||
"unknown": "未测量"
|
||||
},
|
||||
"peopleGroup": "族群",
|
||||
"caveat": "全国数据(世界银行)。各城市的联网数据未公开——请勿用国家数据推断某个城市。",
|
||||
"sheet": {
|
||||
"close": "关闭",
|
||||
"ai": "这些数据来自人工智能深度检索,可能包含错误信息。",
|
||||
"dismiss": "忽略",
|
||||
"internetUsers": "互联网用户",
|
||||
"measured": "测量年份",
|
||||
"mobilePer100": "每百人移动电话",
|
||||
"peopleGroups": "族群",
|
||||
"topPlatform": "主要平台",
|
||||
"unknown": "未知",
|
||||
"carriers": "运营商和互联网服务商({n})",
|
||||
"socialPlatforms": "社交平台",
|
||||
"note": "这是全国数据,不是城市测量值。运营商名称来自 ASN 注册持有者——其中除互联网服务商外还包括银行和大学,因此“运营商”标记是基于名称的推断,并非经过核实的分类。"
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "菜单即将推出",
|
||||
"pinHint": {
|
||||
"tapDot": "点按一个彩色圆点——仍有人需要祷告"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "我的 UPG 100 名單 — 禱告名單與海報",
|
||||
"upg100WallTag": "禱告動員",
|
||||
"upg100WallIntro": "將禱告的工作分成五十人、一百人一組 — 就像耶穌餵飽五千人那樣(路加福音 9:14)。每一張海報都是一份 100 人名單,你可以為他們禱告、列印出來、貼在牆上 — <strong>按著他們族群的名字</strong>為他們禱告。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "国家",
|
||||
"upg100Region": "区域",
|
||||
"upg100Zone": "UPG-100 名单",
|
||||
"upg100Group": "族群"
|
||||
}
|
||||
"upg100Group": "族群",
|
||||
"selectView": "選擇檢視",
|
||||
"views": "檢視",
|
||||
"prayerResources": "禱告資源",
|
||||
"map": "地圖"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,8 @@
|
|||
"check:shared-reached": "node internal/check-shared-component-reached-bundles.mjs",
|
||||
"check:tree-weight": "node internal/check-published-tree-weight.mjs",
|
||||
"check:i18n-never-translated": "node internal/check-i18n-never-translated.mjs",
|
||||
"check:embed-base": "node internal/check-embed-base-resolution.mjs"
|
||||
"check:embed-base": "node internal/check-embed-base-resolution.mjs",
|
||||
"check:composed-filters": "node internal/check-gospel-composed-filters.mjs"
|
||||
},
|
||||
"//scripts": {
|
||||
"_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.",
|
||||
|
|
@ -283,7 +284,8 @@
|
|||
"check:i18n-never-translated": "a key PRESENT in all 11 catalogs but IDENTICAL in all of them is copied, not translated. check:i18n-parity asserts presence; presence and translatedness are different properties. Carries an allowlist for keys correctly identical everywhere (pure format strings, brand names, ISO designations) — each with a reason, so the list records decisions rather than silencing findings.",
|
||||
"check:embed-base": "the shared URL resolver, pinned by ARITHMETIC — no browser, no network, no served tree, so it cannot SKIP itself into irrelevance. It computes what a leaf's own \"../../assets\" means and asserts the resolver agrees. Caught nothing for fifteen ticks because it did not exist: resolveAssetsBase computed one segment too high, every local gate passed (the dev web-root has assets BOTH inside and beside the tree), and the byte-level publish gates went green on the broken publish.",
|
||||
"check:map-unmount-race": "browser-free, sub-second: runs the shipped ensureEngineCSS with a null element (what an unmounted pane hands it) against a copy with its guard stripped; the guarded arm must resolve and the stripped arm must throw \"reading getRootNode\". By gospel-plugin, adopted by studio.",
|
||||
"check:map-unmount-mid-init": "end-to-end, deterministic: holds mapbox-gl.js, opens a map plugin in the studio, switches away (destroying the pane) mid-initialise, then releases the script. Fails on any page error. REQUIRES --url. Exit 2 if the race was not staged."
|
||||
"check:map-unmount-mid-init": "end-to-end, deterministic: holds mapbox-gl.js, opens a map plugin in the studio, switches away (destroying the pane) mid-initialise, then releases the script. Fails on any page error. REQUIRES --url. Exit 2 if the race was not staged.",
|
||||
"check:composed-filters": "CELL C — the gospel map composed above its own tables. Asserts the composite keeps the reader INSIDE it: \"Every resource in <language>\" must be a <button> that filters in place, not an <a> that navigates away and throws out the map the reader just positioned. Checks the tag, that the URL did not change, that the table NARROWED (row count down but not to zero), that the pin count survived, and that nothing 4xx'd. Picks a tier-2 pin from the live source — the first version opened pin 0, a group with nothing found, and failed a working composite on an empty table. Browser gate: run under both rendezvous locks, product lock first."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.6.9",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@
|
|||
* carrier flag is a NAME-PATTERN HEURISTIC, not a verified classification.
|
||||
*/
|
||||
import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import enConnectivity from '@map/i18n/locales/en/connectivity.json'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { addMapAttribution } from '@map/composables/useMapAttribution'
|
||||
|
||||
|
|
@ -151,12 +153,25 @@ function paintGroups(map) {
|
|||
}
|
||||
}
|
||||
|
||||
// ── UI text: library/i18n/locales/<lc>/connectivity.json, in all eleven locales ─────────
|
||||
// i18n is OPTIONAL (the gospel-resources pattern): useI18n() throws on a host that never
|
||||
// installed it, so English from the shipped catalog is the fallback, never a blank or a key.
|
||||
// Carrier and platform names are data and are not translated.
|
||||
const enLookup = (key) =>
|
||||
String(key).replace(/^connectivity\./, '').split('.').reduce((o, k) => (o == null ? o : o[k]), enConnectivity)
|
||||
const fill = (str, params) => (params ? String(str).replace(/\{(\w+)\}/g, (m, k) => (k in params ? params[k] : m)) : str)
|
||||
let t = (key, params) => fill(enLookup(key) ?? String(key), params)
|
||||
try {
|
||||
const i18n = useI18n()
|
||||
t = (key, params) => { const v = i18n.t(key, params || {}); return v === key ? fill(enLookup(key) ?? key, params) : v }
|
||||
} catch (_) { /* no i18n on this host: the English catalog above is the fallback */ }
|
||||
|
||||
// Bands, not a continuous ramp: a reader can hold four categories, not a gradient.
|
||||
const BANDS = [
|
||||
{ id: 'yes', label: 'connected 60%+', color: '#10b981' },
|
||||
{ id: 'partial', label: 'partial 25–59%', color: '#e8c15a' },
|
||||
{ id: 'no', label: 'low, under 25%', color: '#b42318' },
|
||||
{ id: 'unknown', label: 'not measured', color: '#9aa2ad' },
|
||||
{ id: 'yes', color: '#10b981' },
|
||||
{ id: 'partial', color: '#e8c15a' },
|
||||
{ id: 'no', color: '#b42318' },
|
||||
{ id: 'unknown', color: '#9aa2ad' },
|
||||
]
|
||||
const bandColor = (b) => (BANDS.find((x) => x.id === b) || {}).color || '#9aa2ad'
|
||||
|
||||
|
|
@ -293,13 +308,15 @@ useShadowStyles(
|
|||
// correctly: `left:8px; width:var(--map-legend-width,380px)`. Bounded and
|
||||
// left-anchored cannot reach the right rail, so the two can never overlap
|
||||
// again — a geometric guarantee, not a tuned z-index.
|
||||
'.gm-legend{position:absolute;top:8px;left:8px;width:var(--map-legend-width,380px);' +
|
||||
// LOGICAL, not physical: the control rail sits at inset-inline-end, so in Arabic it moves to the
|
||||
// LEFT; a legend pinned left:8px then sat under the zoom buttons (measured in the ar gallery tab).
|
||||
'.gm-legend{position:absolute;top:8px;inset-inline-start:8px;width:var(--map-legend-width,380px);' +
|
||||
'max-width:calc(100% - 16px);box-sizing:border-box;z-index:4;padding:8px 10px;' +
|
||||
'border-radius:10px;background:rgba(255,255,255,.94);color:#0f1419;' +
|
||||
"font:12px/1.35 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;box-shadow:0 2px 10px rgba(0,0,0,.16)}" +
|
||||
// Narrow screens: the rail is still on the right, so the legend must stop short
|
||||
// of it rather than run under it. 46px = 32px button + 3px inset + clearance.
|
||||
'@media(max-width:480px){.gm-legend{width:auto;right:46px}}' +
|
||||
'@media(max-width:480px){.gm-legend{width:auto;inset-inline-end:46px}}' +
|
||||
'.is-dark .gm-legend{background:rgba(19,32,25,.95);color:#e5e7eb}' +
|
||||
'.gm-title{font-weight:700;font-size:12.5px;margin-bottom:5px}' +
|
||||
'.gm-keys{display:flex;flex-wrap:wrap;gap:5px 12px}' +
|
||||
|
|
@ -310,7 +327,7 @@ useShadowStyles(
|
|||
'.gm-sheet{position:absolute;left:0;right:0;bottom:0;z-index:6;max-height:62%;overflow-y:auto;' +
|
||||
'background:#fff;color:#0f1419;border-radius:14px 14px 0 0;padding:14px 16px 22px;' +
|
||||
"font:13px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;box-shadow:0 -6px 26px rgba(0,0,0,.28)}" +
|
||||
'@media(min-width:700px){.gm-sheet{left:auto;right:14px;bottom:14px;width:330px;border-radius:12px}}' +
|
||||
'@media(min-width:700px){.gm-sheet{inset-inline-start:auto;inset-inline-end:14px;bottom:14px;width:330px;border-radius:12px}}' +
|
||||
'.is-dark .gm-sheet{background:#132019;color:#e5e7eb}' +
|
||||
'.gm-sh-head{display:flex;align-items:center;gap:10px;margin-bottom:8px}' +
|
||||
'.gm-sh-name{font-weight:800;font-size:15px}' +
|
||||
|
|
@ -353,17 +370,17 @@ useShadowStyles(
|
|||
</MapToolbar>
|
||||
|
||||
<div v-show="!groupOpen" class="gm-legend">
|
||||
<div class="gm-title">Internet access where unengaged people groups live</div>
|
||||
<div class="gm-title">{{ t('connectivity.title') }}</div>
|
||||
<div class="gm-keys">
|
||||
<span v-for="b in BANDS" :key="b.id" class="gm-key">
|
||||
<i class="gm-sw" :style="{ background: b.color }"></i>{{ b.label }}
|
||||
<i class="gm-sw" :style="{ background: b.color }"></i>{{ t('connectivity.bands.' + b.id) }}
|
||||
</span>
|
||||
<span v-if="groups.length" class="gm-key">
|
||||
<i class="gm-dot" :style="{ background: PG_DOT }"></i>people group ({{ fmt(groups.length) }})
|
||||
<i class="gm-dot" :style="{ background: PG_DOT }"></i>{{ t('connectivity.peopleGroup') }} ({{ fmt(groups.length) }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="gm-caveat">
|
||||
NATIONAL figures (World Bank). Per-city connectivity is not publicly available — do not read a city off a country.
|
||||
{{ t('connectivity.caveat') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -381,29 +398,26 @@ useShadowStyles(
|
|||
<div v-if="selected" class="gm-sheet">
|
||||
<div class="gm-sh-head">
|
||||
<span class="gm-sh-name">{{ selected.country }}</span>
|
||||
<button type="button" class="gm-x" @click="selected = null">Close</button>
|
||||
<button type="button" class="gm-x" @click="selected = null">{{ t('connectivity.sheet.close') }}</button>
|
||||
</div>
|
||||
<div v-if="aiNoticeOpen" class="gm-ai">
|
||||
<span>Data was found by AI deep research — it is subject to false information.</span>
|
||||
<button type="button" class="gm-ai-x" @click="aiNoticeOpen = false" aria-label="Dismiss">×</button>
|
||||
<span>{{ t('connectivity.sheet.ai') }}</span>
|
||||
<button type="button" class="gm-ai-x" @click="aiNoticeOpen = false" :aria-label="t('connectivity.sheet.dismiss')">×</button>
|
||||
</div>
|
||||
<div class="gm-stat"><span>Internet users</span><b>{{ selected.internet_pct != null ? selected.internet_pct + '%' : 'unknown' }}</b></div>
|
||||
<div class="gm-stat"><span>Measured</span><b>{{ selected.internet_year || '—' }}</b></div>
|
||||
<div class="gm-stat"><span>Mobile per 100</span><b>{{ selected.mobile_per_100 ?? '—' }}</b></div>
|
||||
<div class="gm-stat"><span>People groups</span><b>{{ fmt(selected.people_groups) }}</b></div>
|
||||
<div class="gm-stat"><span>Top platform</span><b>{{ selected.top_platform || 'unknown' }}<template v-if="selected.top_platform_pct"> · {{ selected.top_platform_pct }}%</template></b></div>
|
||||
<div class="gm-stat"><span>{{ t('connectivity.sheet.internetUsers') }}</span><b>{{ selected.internet_pct != null ? selected.internet_pct + '%' : t('connectivity.sheet.unknown') }}</b></div>
|
||||
<div class="gm-stat"><span>{{ t('connectivity.sheet.measured') }}</span><b>{{ selected.internet_year || '—' }}</b></div>
|
||||
<div class="gm-stat"><span>{{ t('connectivity.sheet.mobilePer100') }}</span><b>{{ selected.mobile_per_100 ?? '—' }}</b></div>
|
||||
<div class="gm-stat"><span>{{ t('connectivity.sheet.peopleGroups') }}</span><b>{{ fmt(selected.people_groups) }}</b></div>
|
||||
<div class="gm-stat"><span>{{ t('connectivity.sheet.topPlatform') }}</span><b>{{ selected.top_platform || t('connectivity.sheet.unknown') }}<template v-if="selected.top_platform_pct"> · {{ selected.top_platform_pct }}%</template></b></div>
|
||||
<div v-if="selected.carriers && selected.carriers.length" class="gm-list">
|
||||
<div class="gm-list-h">Carriers & ISPs ({{ selected.carrier_count }})</div>
|
||||
<div class="gm-list-h">{{ t('connectivity.sheet.carriers', { n: selected.carrier_count }) }}</div>
|
||||
<ul><li v-for="(c, i) in selected.carriers" :key="i">{{ c }}</li></ul>
|
||||
</div>
|
||||
<div v-if="selected.platforms && selected.platforms.length" class="gm-list">
|
||||
<div class="gm-list-h">Social platforms</div>
|
||||
<div class="gm-list-h">{{ t('connectivity.sheet.socialPlatforms') }}</div>
|
||||
<ul><li v-for="(p, i) in selected.platforms" :key="i">{{ p.name }} · {{ p.pct }}%</li></ul>
|
||||
</div>
|
||||
<p class="gm-note">
|
||||
National figures, not city measurements. Carrier names are ASN registry holders — that set includes
|
||||
banks and universities as well as ISPs, so the carrier flag is a name heuristic, not a verified classification.
|
||||
</p>
|
||||
<p class="gm-note">{{ t('connectivity.sheet.note') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
* IDEA-POSTER-EXPORT-404-ON-DROPPED-FOLDER-TRADEOFF.
|
||||
*/
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PosterWall from '@map/components/poster/PosterWall.vue'
|
||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||
// Named imports: only the list arrays ship in the bundle; the file's bookkeeping fields stay out.
|
||||
|
|
@ -151,9 +152,20 @@ async function fetchListRecords(list) {
|
|||
|
||||
// The wall's own copy. v-html in the shared component, so the <strong> lands as markup —
|
||||
// the emphasis is the point: praying for them BY NAME is the whole brief.
|
||||
const intro = 'Hand away the prayer task in clusters of fifties and hundreds — the way Jesus fed '
|
||||
+ 'the 5,000 (Luke 9:14). Each poster is a 100-list you can pray, print, and put on a wall — '
|
||||
+ 'praying for them <strong>by their people group name</strong>.'
|
||||
//
|
||||
// ★ THROUGH t(), NOT A LITERAL. rega measured ?lang=ar and found this intro, the heading and
|
||||
// the tag rendering in English while the wall's buttons beside them were translated — the
|
||||
// copy a reader reads FIRST was the untranslated part. The <strong> travels inside the
|
||||
// translated string so each language can emphasise its own words, which is not something a
|
||||
// wrapper around an English sentence could do.
|
||||
// Guarded like every other useI18n() here: it throws on a host without vue-i18n.
|
||||
const humanizeKey = (k) => String(k).split('.').pop()
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase())
|
||||
let t = (k) => humanizeKey(k)
|
||||
try { t = useI18n().t } catch (_) { /* no i18n on this host */ }
|
||||
const intro = computed(() => t('messages.upg100WallIntro'))
|
||||
const heading = computed(() => t('messages.upg100WallHeading'))
|
||||
const wallTag = computed(() => t('messages.upg100WallTag'))
|
||||
|
||||
</script>
|
||||
|
||||
|
|
@ -165,8 +177,8 @@ const intro = 'Hand away the prayer task in clusters of fifties and hundreds —
|
|||
:is-dark="isDark"
|
||||
:assets-base="assetsBase"
|
||||
event-name="doxa-upg-100-list:show-on-map"
|
||||
heading="My UPG 100 List — Prayer Lists & Posters"
|
||||
tag="Prayer Mobilization"
|
||||
:heading="heading"
|
||||
:tag="wallTag"
|
||||
:intro="intro"
|
||||
unit="UPG"
|
||||
item-noun="list"
|
||||
|
|
|
|||
|
|
@ -1159,15 +1159,25 @@ provide('onLegendExport', onLegendExport)
|
|||
// The pages themselves come from library/packet/packet-render.js — the SAME module
|
||||
// the build-time generator uses, so the printed packet and the generated one cannot
|
||||
// drift. The card asked for exactly that: reuse, do not grow a fourth print path.
|
||||
// ★ PRINT STRAIGHT FROM THE MAP — the Driver's "that's the only way that map would be usable".
|
||||
// Select a UPG-100 list in the legend and its print options appear at the legend's top right.
|
||||
//
|
||||
// ⚠ THE POSTER PATH USED TO HAND-ROLL AN <a download> OF THE .webp, which was wrong twice:
|
||||
// it SILENTLY DOWNLOADED a file (ruled out 2026-09-23 — "no silent file download as the
|
||||
// default action"), and it had no fallback, so for the lists whose .webp is not shipped it
|
||||
// saved a 404. The wall had a correct implementation the whole time; it was private to
|
||||
// PosterWall.vue, so this surface could not reach it. It now lives in usePosterPrints
|
||||
// (printPoster) and BOTH surfaces call the one copy — which is what the card asked for:
|
||||
// "reuse the existing three print actions rather than growing a fourth implementation."
|
||||
async function onLegendExport(kind) {
|
||||
const zoneId = selectedKey.value;
|
||||
if (!zoneId) return; // the button only shows with a selection
|
||||
if (kind === 'packet') { await printPrayerPacket(zoneId); return; }
|
||||
const base = resolveAssetsBase(profileConfig.value);
|
||||
const a = document.createElement('a');
|
||||
a.href = `${base}/posters/${zoneId}.webp`;
|
||||
a.download = `doxa-upg-100-${zoneId}-poster.webp`;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
// The legend selects by KEY, while the print actions label by item — hand over the zone
|
||||
// object so the print dialog's title says the list's name rather than its slug.
|
||||
// Same resolution printPrayerPacket uses, so both print actions name the list identically.
|
||||
const zone = allLists().find((z) => z.id === zoneId) || { id: zoneId };
|
||||
await packetPrints.printPoster(zone);
|
||||
}
|
||||
|
||||
// Build and print a zone's prayer packet without leaving the page.
|
||||
|
|
@ -1186,6 +1196,12 @@ const packetPrints = usePosterPrints({
|
|||
records: (zone) => packetRecords(zone),
|
||||
lang: () => packetLang(),
|
||||
label: (zone) => zone?.label || zone?.id || '',
|
||||
// Where THIS bundle serves its posters from. printPoster needs it to find
|
||||
// posters/<id>.webp and, when that is not shipped, posters/<id>.html to rasterise.
|
||||
// Resolved here rather than inside the shared action, which knows nothing about a
|
||||
// bundle's asset layout — the same seam printPacket uses for its map svg.
|
||||
assetsBase: () => resolveAssetsBase(profileConfig.value),
|
||||
downloadPrefix: 'doxa-upg-100',
|
||||
});
|
||||
|
||||
// ⚠ BRIDGE THE PROGRESS BACK TO THE SURFACE THIS MAP ACTUALLY SHOWS.
|
||||
|
|
|
|||
|
|
@ -34,11 +34,25 @@
|
|||
* whole MFE (both panes) is what gets shared/embedded.
|
||||
*/
|
||||
import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pane-wrapper.js'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import ViewSelect from '@map/components/ViewSelect.vue'
|
||||
import ShareButton from '@map/components/map-controls/ShareButton.vue'
|
||||
|
||||
// ★ THE SHELL'S OWN CHROME WAS ENGLISH IN EVERY LOCALE. rega measured ?lang=ar and found
|
||||
// 'Select View', 'Prayer Resources' and 'Map' rendering in English while the poster wall's own
|
||||
// buttons inside the panes translated correctly — because the panes each install i18n and this
|
||||
// wrapper never did. The chrome a reader sees FIRST was the untranslated part.
|
||||
//
|
||||
// ⚠ GUARDED, like every other useI18n() call in this codebase: it THROWS on a host that has not
|
||||
// installed vue-i18n, and this component is mounted by hosts we do not control. The fallback
|
||||
// humanises the key so a bare host shows "Select View", never a blank or a raw dotted key.
|
||||
const humanizeKey = (k) => String(k).split('.').pop()
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase())
|
||||
let t = (k) => humanizeKey(k)
|
||||
try { t = useI18n().t } catch (_) { /* no i18n on this host */ }
|
||||
|
||||
const cfg = inject('profileConfig', ref(null))
|
||||
const tk = inject('mapboxToken', ref(''))
|
||||
const uiStore = inject('uiStore', null)
|
||||
|
|
@ -66,9 +80,11 @@ const spec = computed(() => ({
|
|||
// only one of the two desynchronises the tab strip from the scroll column — the
|
||||
// strip would read "Prayer Resources | Map" while scroll position 0 still held the
|
||||
// map. Both are swapped here, and `view` below is initialised from the same sort.
|
||||
// Labels through t(). `spec` is a computed, so switching locale re-labels the tab strip
|
||||
// rather than needing a remount — which is the behaviour the locale switcher assumes.
|
||||
panes: [
|
||||
{ id: 'list', label: 'Prayer Resources', at: [0, 0], config: { profile: 'poster-wall' } },
|
||||
{ id: 'map', label: 'Map', at: [1, 0], config: { profile: 'upg-100-list' } },
|
||||
{ id: 'list', label: t('tabs.prayerResources'), at: [0, 0], config: { profile: 'poster-wall' } },
|
||||
{ id: 'map', label: t('tabs.map'), at: [1, 0], config: { profile: 'upg-100-list' } },
|
||||
],
|
||||
// The View switcher lives in this element's own header, so the layout's
|
||||
// "… below ↓" pill would be a second, redundant control (shell design note 2026-08-01).
|
||||
|
|
@ -96,6 +112,11 @@ const view = computed({
|
|||
})
|
||||
let column = null
|
||||
let offScroll = null
|
||||
// The mounted custom element per pane id. Kept because a cross-pane action has to reach the
|
||||
// TARGET PANE'S ELEMENT: upg-100-list listens for 'doxa:select-zone' on itself rather than on
|
||||
// window, precisely so two embeds on one page do not cross-talk — which means the shell has to
|
||||
// know which element it mounted. Cleared on unmount with everything else.
|
||||
const paneEls = {}
|
||||
|
||||
// PAGE-LEVEL config keys a pane INHERITS from the host page.
|
||||
//
|
||||
|
|
@ -138,11 +159,30 @@ function go(id) {
|
|||
}
|
||||
|
||||
// Cross-pane action (shell seam): the poster wall dispatches a scoped window event
|
||||
// when a card's "Show on map" is clicked. The pane owner scrolls to the map pane.
|
||||
// when a card's "Show on map" is clicked. The pane owner scrolls to the map pane AND
|
||||
// tells the map WHICH zone to isolate.
|
||||
//
|
||||
// ★ SCROLLING WAS ONLY HALF THE HANDOFF, and the missing half was invisible because the
|
||||
// visible part worked: go('map') moved the reader to the map, so the action looked like it
|
||||
// had happened, while the map still showed all 27 zones. The Driver's brief for this is
|
||||
// "when I click Show on map it should be basically the same as if I searched that result in
|
||||
// the search bar" — i.e. ISOLATED, not merely scrolled to.
|
||||
//
|
||||
// The map half was already built and waiting: upg-100-list.vue listens for 'doxa:select-zone'
|
||||
// ON ITS OWN ELEMENT (not on window, so several embeds never cross-talk) and routes it through
|
||||
// onLegendSelect — the same path a legend row tap and the search bar use. So the shell owes it
|
||||
// exactly one re-dispatch, onto the element it mounted, and nothing here re-implements
|
||||
// selection. detail.listId carries the zone, or null to un-isolate, which is what the wall
|
||||
// already sends for its green toggle.
|
||||
function onShowOnMap(e) {
|
||||
const d = e?.detail || {}
|
||||
if (d.instanceId && d.instanceId !== instanceId.value) return
|
||||
go('map')
|
||||
// The element is the target, so a pane that has not mounted yet simply gets nothing rather
|
||||
// than throwing — the scroll still happens, which is the degradation we want.
|
||||
paneEls.map?.dispatchEvent(new CustomEvent('doxa:select-zone', {
|
||||
detail: { listId: d.listId ?? null },
|
||||
}))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -151,7 +191,9 @@ onMounted(() => {
|
|||
if (!column) return
|
||||
for (const pane of column.panes) {
|
||||
if (!pane.config) continue
|
||||
column.mountEl(pane.id)?.appendChild(paneElement(pane))
|
||||
const el = paneElement(pane)
|
||||
paneEls[pane.id] = el
|
||||
column.mountEl(pane.id)?.appendChild(el)
|
||||
}
|
||||
// Tabs are a readout as well as a control — follow the user's scroll.
|
||||
offScroll = column.onScroll(() => {
|
||||
|
|
@ -166,6 +208,9 @@ onBeforeUnmount(() => {
|
|||
offScroll?.()
|
||||
column?.destroy()
|
||||
column = null
|
||||
// Drop the element references with the column that owned them — holding a detached custom
|
||||
// element after unmount is a leak, and a stale one would be dispatched into on remount.
|
||||
for (const k of Object.keys(paneEls)) delete paneEls[k]
|
||||
})
|
||||
|
||||
// Share the WHOLE MFE: this composite profile's own leaf page, TREE-RELATIVE
|
||||
|
|
@ -222,8 +267,8 @@ useShadowStyles(PANE_GRID_STYLES + `
|
|||
<ViewSelect
|
||||
:views="spec.panes"
|
||||
:current="view"
|
||||
legend="Select View"
|
||||
label="Views"
|
||||
:legend="t('tabs.selectView')"
|
||||
:label="t('tabs.views')"
|
||||
@select="go"
|
||||
/>
|
||||
<ShareButton :is-dark="isDark" :embed-path="SHARE_EMBED_PATH" />
|
||||
|
|
|
|||
|
|
@ -82,7 +82,16 @@ let offScroll = null
|
|||
// own default. Forwarded by KEY LIST, never by spreading the whole host config, so a pane's own
|
||||
// `profile` / `instanceId` cannot be overwritten by the host's — that is what makes a pane a
|
||||
// distinct view at all (pane.config is spread last and always wins).
|
||||
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'prayBaseUrl', 'locale', 'lang', 'languageCode', 'isDark', 'qa', 'pinHint']
|
||||
// ★ urlSync IS PAGE-SCOPED CONFIG AND MUST BE INHERITED. Landing the guard in
|
||||
// region-dashboards.vue was necessary and NOT sufficient: a nested pane has no leaf page of its
|
||||
// own, so it only sees what this composite forwards. Without `urlSync` in this list the panes read
|
||||
// cfg.urlSync as undefined, the guard never fires, and a Regions nested in the Research Gallery
|
||||
// goes on rewriting the HOST page's #anchor and tab title -- which is exactly what rega measured
|
||||
// after my fix landed. A guard you cannot reach is not a guard.
|
||||
// It is the same lesson as the cap-4 asset-path one this file already records: page-scoped config
|
||||
// has to be forwarded explicitly. Note the filter below keeps `false`, which is the value that
|
||||
// matters here -- it rejects undefined, null and '' only.
|
||||
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'prayBaseUrl', 'locale', 'lang', 'languageCode', 'isDark', 'qa', 'pinHint', 'urlSync']
|
||||
|
||||
function paneElement(pane) {
|
||||
const el = document.createElement(TAG)
|
||||
|
|
|
|||
54
public/doxa-maps/cities-internet/cities-internet.js
generated
54
public/doxa-maps/cities-internet/cities-internet.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-rvaIXZcg4jjrxN6XHCvdI+DHoyZA0t2kiYyAThKirIGfpKM2qiFvvXKkEct/FLa3" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-rvaIXZcg4jjrxN6XHCvdI+DHoyZA0t2kiYyAThKirIGfpKM2qiFvvXKkEct/FLa3" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-rvaIXZcg4jjrxN6XHCvdI+DHoyZA0t2kiYyAThKirIGfpKM2qiFvvXKkEct/FLa3" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-5qSCvIBFXPPGtMS/Wxag04PJ5AILGAgCXau+AwIOA+7gXOwIdXxTXAVyYzJfr4w8" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-v7L/+GJ4PJtgHuBHTblSHdMdpwDcU2snwsxbH/+T/hrItI4Nh5MHHcYoNzDhcS/V" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-5qSCvIBFXPPGtMS/Wxag04PJ5AILGAgCXau+AwIOA+7gXOwIdXxTXAVyYzJfr4w8" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-v7L/+GJ4PJtgHuBHTblSHdMdpwDcU2snwsxbH/+T/hrItI4Nh5MHHcYoNzDhcS/V" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-5qSCvIBFXPPGtMS/Wxag04PJ5AILGAgCXau+AwIOA+7gXOwIdXxTXAVyYzJfr4w8" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-v7L/+GJ4PJtgHuBHTblSHdMdpwDcU2snwsxbH/+T/hrItI4Nh5MHHcYoNzDhcS/V" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
244
public/doxa-maps/country-posters/country-posters.js
generated
244
public/doxa-maps/country-posters/country-posters.js
generated
File diff suppressed because one or more lines are too long
60
public/doxa-maps/doxa-simple-map/doxa-simple-map.js
generated
60
public/doxa-maps/doxa-simple-map/doxa-simple-map.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-OMLEhQZDOxDlsePrZj3SUJD9kGm2HXF8B/C+nmKtgB+cc4KGKZtYkFMxAXf/hyum" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-UNTsuIGRBdH3u7q8NOdFm7YZCgyfNseZ35e5Z9sipJQo7m6cnmz3vEU19jehgtcc" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-OMLEhQZDOxDlsePrZj3SUJD9kGm2HXF8B/C+nmKtgB+cc4KGKZtYkFMxAXf/hyum" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-UNTsuIGRBdH3u7q8NOdFm7YZCgyfNseZ35e5Z9sipJQo7m6cnmz3vEU19jehgtcc" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../doxa-simple-map.js" integrity="sha384-OMLEhQZDOxDlsePrZj3SUJD9kGm2HXF8B/C+nmKtgB+cc4KGKZtYkFMxAXf/hyum" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-simple-map.js" integrity="sha384-UNTsuIGRBdH3u7q8NOdFm7YZCgyfNseZ35e5Z9sipJQo7m6cnmz3vEU19jehgtcc" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-OMLEhQZDOxDlsePrZj3SUJD9kGm2HXF8B/C+nmKtgB+cc4KGKZtYkFMxAXf/hyum" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-UNTsuIGRBdH3u7q8NOdFm7YZCgyfNseZ35e5Z9sipJQo7m6cnmz3vEU19jehgtcc" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
168
public/doxa-maps/key-cities/key-cities.js
generated
168
public/doxa-maps/key-cities/key-cities.js
generated
File diff suppressed because one or more lines are too long
36
public/doxa-maps/manifest.json
generated
36
public/doxa-maps/manifest.json
generated
|
|
@ -105,7 +105,7 @@
|
|||
"bundle": "cities-internet",
|
||||
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||
"output": "doxa-maps/cities-internet/cities-internet.js",
|
||||
"integrity": "sha384-rvaIXZcg4jjrxN6XHCvdI+DHoyZA0t2kiYyAThKirIGfpKM2qiFvvXKkEct/FLa3",
|
||||
"integrity": "sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX",
|
||||
"elements": [
|
||||
"cities-internet-map"
|
||||
],
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./cities-internet/connectivity-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rvaIXZcg4jjrxN6XHCvdI+DHoyZA0t2kiYyAThKirIGfpKM2qiFvvXKkEct/FLa3\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./cities-internet/connectivity-map/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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rvaIXZcg4jjrxN6XHCvdI+DHoyZA0t2kiYyAThKirIGfpKM2qiFvvXKkEct/FLa3\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -149,7 +149,7 @@
|
|||
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./cities-internet/data-tables/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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rvaIXZcg4jjrxN6XHCvdI+DHoyZA0t2kiYyAThKirIGfpKM2qiFvvXKkEct/FLa3\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -158,7 +158,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "doxa-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-5qSCvIBFXPPGtMS/Wxag04PJ5AILGAgCXau+AwIOA+7gXOwIdXxTXAVyYzJfr4w8",
|
||||
"integrity": "sha384-v7L/+GJ4PJtgHuBHTblSHdMdpwDcU2snwsxbH/+T/hrItI4Nh5MHHcYoNzDhcS/V",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -178,7 +178,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-5qSCvIBFXPPGtMS/Wxag04PJ5AILGAgCXau+AwIOA+7gXOwIdXxTXAVyYzJfr4w8\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-v7L/+GJ4PJtgHuBHTblSHdMdpwDcU2snwsxbH/+T/hrItI4Nh5MHHcYoNzDhcS/V\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -192,7 +192,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-5qSCvIBFXPPGtMS/Wxag04PJ5AILGAgCXau+AwIOA+7gXOwIdXxTXAVyYzJfr4w8\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-v7L/+GJ4PJtgHuBHTblSHdMdpwDcU2snwsxbH/+T/hrItI4Nh5MHHcYoNzDhcS/V\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -206,7 +206,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-5qSCvIBFXPPGtMS/Wxag04PJ5AILGAgCXau+AwIOA+7gXOwIdXxTXAVyYzJfr4w8\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-v7L/+GJ4PJtgHuBHTblSHdMdpwDcU2snwsxbH/+T/hrItI4Nh5MHHcYoNzDhcS/V\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -396,7 +396,7 @@
|
|||
"bundle": "doxa-simple-map",
|
||||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"output": "doxa-maps/doxa-simple-map/doxa-simple-map.js",
|
||||
"integrity": "sha384-OMLEhQZDOxDlsePrZj3SUJD9kGm2HXF8B/C+nmKtgB+cc4KGKZtYkFMxAXf/hyum",
|
||||
"integrity": "sha384-UNTsuIGRBdH3u7q8NOdFm7YZCgyfNseZ35e5Z9sipJQo7m6cnmz3vEU19jehgtcc",
|
||||
"elements": [
|
||||
"doxa-map",
|
||||
"doxa-simple-map"
|
||||
|
|
@ -413,7 +413,7 @@
|
|||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-OMLEhQZDOxDlsePrZj3SUJD9kGm2HXF8B/C+nmKtgB+cc4KGKZtYkFMxAXf/hyum\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-UNTsuIGRBdH3u7q8NOdFm7YZCgyfNseZ35e5Z9sipJQo7m6cnmz3vEU19jehgtcc\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -681,7 +681,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO",
|
||||
"integrity": "sha384-whz8Z7PqwEA70At6cogijrOrFy7J4VPReCyUVO3iyzWQEBe3ZZnEoRXA5g9JR+7K",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -701,7 +701,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"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>",
|
||||
"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-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"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-whz8Z7PqwEA70At6cogijrOrFy7J4VPReCyUVO3iyzWQEBe3ZZnEoRXA5g9JR+7K\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -715,7 +715,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"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>",
|
||||
"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-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"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-whz8Z7PqwEA70At6cogijrOrFy7J4VPReCyUVO3iyzWQEBe3ZZnEoRXA5g9JR+7K\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -729,7 +729,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"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>",
|
||||
"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-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"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-whz8Z7PqwEA70At6cogijrOrFy7J4VPReCyUVO3iyzWQEBe3ZZnEoRXA5g9JR+7K\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -879,7 +879,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-loMRSunQDFyzu8MiF10LCkcUXriEGb12F3mHbCvVPLR3ayOyUgqERAimXn2fz2fZ",
|
||||
"integrity": "sha384-DClm3BV580b9LYTon03X8XJD4uza6w2fKBPQTMpeIugtHgyqZXqpoCg88c+d6i1c",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -899,7 +899,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-loMRSunQDFyzu8MiF10LCkcUXriEGb12F3mHbCvVPLR3ayOyUgqERAimXn2fz2fZ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-DClm3BV580b9LYTon03X8XJD4uza6w2fKBPQTMpeIugtHgyqZXqpoCg88c+d6i1c\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -913,7 +913,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-loMRSunQDFyzu8MiF10LCkcUXriEGb12F3mHbCvVPLR3ayOyUgqERAimXn2fz2fZ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-DClm3BV580b9LYTon03X8XJD4uza6w2fKBPQTMpeIugtHgyqZXqpoCg88c+d6i1c\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -927,7 +927,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-loMRSunQDFyzu8MiF10LCkcUXriEGb12F3mHbCvVPLR3ayOyUgqERAimXn2fz2fZ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-DClm3BV580b9LYTon03X8XJD4uza6w2fKBPQTMpeIugtHgyqZXqpoCg88c+d6i1c\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
204
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
204
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-whz8Z7PqwEA70At6cogijrOrFy7J4VPReCyUVO3iyzWQEBe3ZZnEoRXA5g9JR+7K" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-whz8Z7PqwEA70At6cogijrOrFy7J4VPReCyUVO3iyzWQEBe3ZZnEoRXA5g9JR+7K" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-mb0a3w3W6EiyAv5tQ6SpKpEbigF2HIArUes+z4SG2uobtD+siUkdkTLvBAzqSidO" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-whz8Z7PqwEA70At6cogijrOrFy7J4VPReCyUVO3iyzWQEBe3ZZnEoRXA5g9JR+7K" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-loMRSunQDFyzu8MiF10LCkcUXriEGb12F3mHbCvVPLR3ayOyUgqERAimXn2fz2fZ" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-DClm3BV580b9LYTon03X8XJD4uza6w2fKBPQTMpeIugtHgyqZXqpoCg88c+d6i1c" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
180
public/doxa-maps/regions-dashboard/regions-dashboard.js
generated
180
public/doxa-maps/regions-dashboard/regions-dashboard.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-loMRSunQDFyzu8MiF10LCkcUXriEGb12F3mHbCvVPLR3ayOyUgqERAimXn2fz2fZ" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-DClm3BV580b9LYTon03X8XJD4uza6w2fKBPQTMpeIugtHgyqZXqpoCg88c+d6i1c" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-loMRSunQDFyzu8MiF10LCkcUXriEGb12F3mHbCvVPLR3ayOyUgqERAimXn2fz2fZ" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-DClm3BV580b9LYTon03X8XJD4uza6w2fKBPQTMpeIugtHgyqZXqpoCg88c+d6i1c" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue