Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run

# Conflicts:
#	public/doxa-maps/index.html
This commit is contained in:
doxa-maps-88655c70 2026-09-24 22:12:10 +00:00
commit 90f0c37ddf
53 changed files with 503 additions and 297 deletions

View file

@ -99,11 +99,12 @@ One `<iframe>` tag, no build step. Copy the `embed.iframe` snippet from `manifes
![Method 1 — the prayer map embedded in a host page's iframe](documentation/readme-images/deploy1-iframe.png)
### 2. Mobile app — UPG-100 on a phone 🛠️ in progress
### 2. Mobile app — UPG-100 on a phone ✅ built
The UPG-100 list, shipped on its own as a Capacitor app. It is being finished for phones now;
its screenshot lands here when it is ready. `bun run check:mobile` · `bun run phone` (needs
Android SDK + JDK 17)
The UPG-100 prayer lists and posters, shipped on their own as a Capacitor app: one poster per row,
print options that wrap to fit. `bun run check:mobile` · `bun run phone` (needs Android SDK + JDK 17)
<p align="center"><img src="documentation/readme-images/deploy2-mobile-upg100.png" alt="Method 2 — the UPG-100 list at phone size" width="320"></p>
### 3. JavaScript embed — the Research Gallery on dx-life ✅ production

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1,018 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 KiB

View file

@ -0,0 +1,46 @@
{
"cap1-single-element": {
"bundle": "all-people-groups",
"sha256": "d48c17dc369294301d17a4d29fc517cc7c2c2b18561b6d416ae02a9fbfb60c33"
},
"cap2-adoption": {
"bundle": "doxa-simple-map",
"sha256": "a94db2c47d598dbc59daec6903afaac93862add9c0b7a532891b9c3c0608ce02"
},
"cap2-engagement": {
"bundle": "doxa-simple-map",
"sha256": "a94db2c47d598dbc59daec6903afaac93862add9c0b7a532891b9c3c0608ce02"
},
"cap2-prayer": {
"bundle": "doxa-simple-map",
"sha256": "a94db2c47d598dbc59daec6903afaac93862add9c0b7a532891b9c3c0608ce02"
},
"cap3-monolithic-research": {
"bundle": "doxa-research-map",
"sha256": "eb305958dffd4a067de6f00155c3f396452adfc6d20e842cc9ae4f921e54f44a"
},
"cap4-upg100-list": {
"bundle": "my-upg-100-list",
"sha256": "04e217ca6851d9aec3f9a9331c5105f1f63c60c76c8bc268b9f6c2ea26cf614f"
},
"cap4-upg100-mfe": {
"bundle": "my-upg-100-list",
"sha256": "04e217ca6851d9aec3f9a9331c5105f1f63c60c76c8bc268b9f6c2ea26cf614f"
},
"cap4-upg100-poster-wall": {
"bundle": "my-upg-100-list",
"sha256": "04e217ca6851d9aec3f9a9331c5105f1f63c60c76c8bc268b9f6c2ea26cf614f"
},
"cap5-doxa-maps-spa": {
"bundle": "doxa-maps-spa",
"sha256": "c7b1aad1e3bd4bbe936858f2159c566c1ef2e24fc9636a3446b1e10723d86502"
},
"cap5-plugin-studio": {
"bundle": "plugin-studio",
"sha256": "c3d32d94d2df407fc35cea91faf0d46670abf52a63d37000486760361a588a5c"
},
"deploy2-mobile-upg100": {
"bundle": "my-upg-100-list",
"sha256": "04e217ca6851d9aec3f9a9331c5105f1f63c60c76c8bc268b9f6c2ea26cf614f"
}
}

View file

@ -103,9 +103,15 @@ async function measure(width) {
}
const ctrls = all.filter((e) => /\\b(pbtn|pstock)\\b/.test((e.className||'').toString())).filter(vis)
const tiny = ctrls.filter((e) => { const b = e.getBoundingClientRect(); return b.width < 18 || b.height < 10 })
// FITS is not READABLE, and this gate asserted only the weaker one. A control 40px wide passes
// every size check while showing five characters of a 76px label — measured, 81 of 108 controls
// clipped, with Prayer List and Poster Map both rendering as Pra... A row made to fit by hiding
// what its buttons SAY has not been fixed. scrollWidth > clientWidth is the browser telling us
// the text did not fit its box.
const clipped = ctrls.filter((e) => e.scrollWidth > e.clientWidth + 1)
return { cards: cards.length, rows: cards.filter((c) => c.querySelector('.pfs')).length,
overflowing, worst, intrusions, coveringPreview, worstY,
previewsPainted, previewsBroken, noPreview,
previewsPainted, previewsBroken, noPreview, clipped: clipped.length,
controls: ctrls.length, tiny: tiny.length }
})()`)
await page.close()
@ -128,7 +134,7 @@ for (const r of results) {
+ ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}`
+ ` · previews ${r.previewsPainted} painted`
+ `${r.previewsBroken ? `/${r.previewsBroken} BROKEN` : ''}${r.noPreview ? `/${r.noPreview} none` : ''}`
+ ` · ${r.controls} control(s), ${r.tiny} too small`)
+ ` · ${r.controls} control(s), ${r.tiny} too small, ${r.clipped} clipped`)
}
const fails = []
@ -144,6 +150,13 @@ for (const r of results) {
fails.push(`${at}: ${r.tiny} print control(s) shrank below a usable size — fitting the row by `
+ 'crushing the buttons is not a fix')
}
// AND THE LABELS MUST BE READABLE. Same failure in a subtler form: the control keeps a usable
// BOX while its text is ellipsised away, so a reader cannot tell Prayer List from Poster Map.
if (r.clipped) {
fails.push(`${at}: ${r.clipped} of ${r.controls} print control(s) have CLIPPED labels `
+ '(scrollWidth exceeds clientWidth) — the button is big enough to press and no longer says '
+ 'what it does')
}
// The row belongs BELOW the preview. Driver-reported 2026-09-24: "the download print options are
// not showing up under the blue outline, they are overlapping where the blue outline would be".
// THE PREVIEW MUST ACTUALLY BE THERE. A card with no image passes every geometry assertion

View file

@ -25,6 +25,11 @@
* Shots whose path is not a /doxa-maps/ leaf (the synthetic iframe host) are REPORTED as skipped
* rather than dropped — an unchecked shot must be visible, not invisible.
*
* CONTENT FIRST, CLOCKS ONLY AS A FALLBACK. readme-shots writes shot-bundles.json: for each shot,
* the sha256 of the bundle it was taken from. A matching hash means the picture shows exactly the
* bundle built now; a different one means it is stale. No date is involved, so a clone, a rebase
* or a retake that came out byte-identical cannot fool it. Shots with no record yet fall back to:
*
* WHICH CLOCK: GIT, NOT THE FILESYSTEM, for any file that is committed and unmodified.
* This compared mtimes, and a clone (or a new worktree) writes files in whatever order git checks
* them out — so every fresh checkout failed, pristine main included (11 shots "2s behind" their
@ -33,11 +38,12 @@
* tarball) falls back to its mtime, because then the working copy IS the newest version.
*
* NEGATIVE-TESTED: appending a byte to public/doxa-maps/doxa-simple-map/doxa-simple-map.js makes
* this FAIL naming the three cap2-* shots (a dirty file is dated by mtime); reverting it returns it
* to PASS. A bare `touch` no longer fails it — a touch changes no content, so nothing is stale.
* this FAIL naming exactly the three cap2-* shots (their recorded hash no longer matches);
* reverting it returns it to PASS. A bare `touch` does not fail it: it changes no content.
*/
import { readFileSync, existsSync, statSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
@ -51,6 +57,9 @@ if (!existsSync(SHOTS_SRC) || !existsSync(OUT)) {
process.exit(0)
}
const SHOT_BUNDLES = join(OUT, 'shot-bundles.json')
const recorded = existsSync(SHOT_BUNDLES) ? JSON.parse(readFileSync(SHOT_BUNDLES, 'utf8')) : {}
// Dirty = modified or untracked under the two trees compared. null = git unavailable -> mtimes only.
const git = (...a) => execFileSync('git', ['-C', ROOT, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
let dirty = null
@ -86,6 +95,18 @@ for (const { name, path } of shots) {
if (!existsSync(bundle)) { skipped.push(`${name} (bundle ${m[1]} not built)`); continue }
checked++
// FIRST CHOICE: content, not clocks. readme-shots records the sha256 of the bundle each picture
// was taken from. Same hash = the picture shows exactly this bundle, however old either file's
// date. A different hash = the bundle changed since, so the picture is stale. This also covers
// a retake that came out byte-identical, which no clock can see: git dates an unchanged PNG by
// its old commit, and the gate called a correct, just-taken picture stale.
const rec = recorded[name]
if (rec && rec.bundle === m[1]) {
const now = createHash('sha256').update(readFileSync(bundle)).digest('hex')
if (now !== rec.sha256) stale.push(`${name}.png was shot from a different ${m[1]} bundle than the one built now`)
continue
}
// FALLBACK, for a shot with no record yet: clocks.
const b = stamp(bundle), p = stamp(png)
if (b.ms > p.ms) {
stale.push(`${name}.png is OLDER than the bundle it shows (${m[1]}) — ` +
@ -109,4 +130,4 @@ if (missing.length || stale.length) {
console.error(' A stale screenshot looks exactly like a current one — that is why this is a gate.')
process.exit(1)
}
console.log('SHOT-FRESHNESS: PASS — every screenshot is newer than the bundle it shows.')
console.log('SHOT-FRESHNESS: PASS — every screenshot shows the bundle that is built now.')

View file

@ -12,7 +12,9 @@
*/
import { chromium } from 'playwright'
import { createServer } from 'node:http'
import { readFile, stat, mkdir } from 'node:fs/promises'
import { readFile, stat, mkdir, writeFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs'
import { join, extname, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
@ -86,6 +88,10 @@ const browser = await chromium.launch({ args: [
// ★ A SHOT THAT FAILS MUST NOT SILENTLY PRODUCE A README IMAGE. Collected and asserted at the end.
const failures = []
// documentation/readme-images/shot-bundles.json: shot name -> the sha256 of the bundle it was shot
// from. Merged, so an ONLY= run updates just the shots it took.
const SHOT_BUNDLES = resolve(HERE, '..', 'documentation', 'readme-images', 'shot-bundles.json')
const shotBundles = existsSync(SHOT_BUNDLES) ? JSON.parse(readFileSync(SHOT_BUNDLES, 'utf8')) : {}
const ONLY = process.env.ONLY ? process.env.ONLY.split(',') : null
// mapbox canvas selector to wait on (leaf pages mount a custom element with a canvas)
@ -183,6 +189,13 @@ async function shot(name, path, {
if (clip) opts.clip = clip
await page.screenshot(opts)
console.log(' OK', name + '.png', `<- ${path} (${ready} elements)`)
// Record WHICH BUNDLE this picture is of, by content hash, so check:shots can tell a current
// picture from a stale one without trusting any clock (see SHOT_BUNDLES below).
const lm = path.match(/^\/doxa-maps\/([^/]+)\//)
const bundleJs = lm && join(ROOT, 'doxa-maps', lm[1], `${lm[1]}.js`)
if (bundleJs && existsSync(bundleJs)) {
shotBundles[name] = { bundle: lm[1], sha256: createHash('sha256').update(readFileSync(bundleJs)).digest('hex') }
}
} catch (e) {
console.log(' ERR', name, e.message.split('\n')[0])
failures.push(name + ': ' + e.message.split('\n')[0])
@ -339,13 +352,12 @@ virtualPages.set('/__readme-iframe-host.html', `<!doctype html>
await shot('deploy1-iframe', '/__readme-iframe-host.html',
{ vp: { width: 1000, height: 700 }, wait: 11000, countFrames: true, mapboxReferer: false })
// ---- DEPLOY2: mobile — deliberately NOT shot yet ----
// The mobile example is UPG-100 (ruled 2026-09-24). Simple Research was retired as the example:
// it was never built for phones (legend, a bottom button cut off by the page edge, unreadable
// prompt). Once its phone layout is finished, add a shot named deploy2-mobile-upg100 of
// /doxa-maps/my-upg-100-list/upg-100-mfe/index.html at vp 390x844, dsf 3, and embed it in
// README.md under method 2. (Not written as code here: check:shots parses shot calls, even in
// comments, and would demand a PNG that does not exist yet.)
// ---- DEPLOY2: mobile — the UPG-100 list as the phone app ----
// The mobile example is UPG-100 (ruled 2026-09-24; Simple Research was retired as the example because
// it was never built for phones). Shot once its owner marked the phone layout ready at 88a566f:
// posters one per row, the legend a bottom sheet flush with the map, print options wrapping.
await shot('deploy2-mobile-upg100', '/doxa-maps/my-upg-100-list/upg-100-mfe/index.html',
{ vp: { width: 390, height: 844 }, wait: 10000, dsf: 3 })
// ---- DEPLOY3: JS embed — the Research Gallery on a real host site ----
// The whole cap-5 gallery JS-embedded on dx-life's research page (a <script src> plus one element,
@ -361,6 +373,7 @@ if (process.env.DXLIFE_URL) {
await browser.close()
server.close()
await writeFile(SHOT_BUNDLES, JSON.stringify(Object.fromEntries(Object.entries(shotBundles).sort()), null, 1) + '\n')
// ★ EXIT NON-ZERO ON ANY FAILED SHOT. Previously this printed DONE unconditionally, so a run
// where every map came back blank still reported success and the stale PNGs stayed in documentation/.

View file

@ -607,7 +607,10 @@ useShadowStyles(`
pointer-events: auto;
}
.stl-panel.closed { transform: translateX(-100%); opacity: 0; pointer-events: none; }
.stl-panel.fit-content { max-height: calc(100% - 72px); }
/* Derived from --stl-top for the same reason as the base rule above: a fixed 72px here overflowed
whenever the profile's real offset was larger (UPG-100 sets 58px + a 12px bottom margin = 70px of
required clearance, against 72px only by luck; at --stl-top:8px it wasted 64px of panel height). */
.stl-panel.fit-content { max-height: calc(100% - var(--stl-top, 60px) - 12px); }
.stl-titlebar {
display: flex; align-items: center; gap: 6px;
padding: 8px 12px; flex-shrink: 0;
@ -923,7 +926,21 @@ useShadowStyles(`
.stl-reopen-caret { color: #6e7681; flex-shrink: 0; }
.stl-reopen:hover .stl-reopen-caret { color: #c9d1d9; }
@media (max-width: 640px) {
.stl-panel { width: 92vw; max-width: 380px; top: 64px; max-height: calc(100vh - 72px); }
/* ⚠ TWO BUGS LIVED IN THIS ONE LINE, and both are the same mistake: a number that has to agree
with another number, written out twice.
1. THE CAP MEASURED THE WRONG BOX. calc(100vh - 72px) sizes the panel against the VIEWPORT,
while the panel is positioned inside the MAP (position:absolute within .upg-map{inset:0}).
Those agree only when the map fills the screen — true of every standalone leaf, false the
moment a host puts a header above the map or content below it, which is how the element is
actually embedded. 100% is the map's own box, the same reference the desktop rule uses.
2. THE OFFSET AND THE CAP DISAGREED. top:64px with max-height:calc(... - 72px) subtracts a
constant that has nothing to do with the offset above it. --stl-top makes one value drive
both, so they cannot drift apart again.
MEASURED 2026-09-24 at 390px in a 420px-tall host: the panel ran 38px past the bottom of the
map. Driver: "the upg 100 list legend on the map is overflowing beyond the height of the map." */
.stl-panel { width: 92vw; max-width: 380px; --stl-top: 64px; }
.stl-reopen { top: 64px; }
}
.stl-panel[data-theme="light"] .stl-inner {

View file

@ -39,7 +39,7 @@
* CROSS-PANE COMMS — this wall NEVER imports a map. It dispatches a window
* CustomEvent scoped by instanceId, so several embeds on one page never cross-talk.
*/
import { computed, ref, onBeforeUnmount } from 'vue'
import { computed, ref, watch, onBeforeUnmount } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { usePosterPrints } from '@map/composables/usePosterPrints.js'
import { useI18n } from 'vue-i18n'
@ -146,8 +146,30 @@ const props = defineProps({
// group in its own chrome (the region card's header and summary) would otherwise repeat it,
// and with a single unnamed group the head renders the literal "Other". Default true.
sectionHeads:{ type: Boolean, default: true },
// ★ HOW MANY POSTERS SIT IN A ROW. The Driver, on the regions posters: "i need two posters per
// row on the regiosn posters and witha setitn got icnreat eh number of poswers per row".
// 0 keeps the wall's existing auto-fill (repeat(auto-fill, minmax(210px, 1fr))), so the two
// panes that already ship — country-posters and my-upg-100-list — are byte-identical unless
// their owner opts in. A positive number fixes the track count instead.
columns: { type: Number, default: 0 },
// And the control that changes it, off by default for the same reason. Only the caller that
// asked for a fixed count wants a knob for it.
columnsControl: { type: Boolean, default: false },
})
// The live count, seeded from the prop. A ref rather than the prop directly, because the control
// changes it and a prop is not ours to write.
const perRow = ref(props.columns || 0)
watch(() => props.columns, (n) => { perRow.value = n || 0 })
// The choices the control offers. Two is the Driver's default; the ceiling is the wall's own
// auto-fill behaviour, offered as "fit" so the knob can always get back to where it started.
const PER_ROW_CHOICES = [2, 3, 4, 5, 6]
// 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))` }
: {})
const plural = computed(() => props.itemNounPlural || `${props.itemNoun}s`)
const base = computed(() => String(props.assetsBase || './assets').replace(/\/+$/, ''))
@ -436,8 +458,22 @@ 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.
'.grid{display:grid;gap:64px 30px;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));' +
// ⚠ 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));' +
'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
// itself. max-width matches the grid's, so the two share an edge.
'.perrow{display:flex;align-items:center;justify-content:flex-end;gap:8px;' +
'max-width:1180px;margin:0 auto 12px;font:inherit;font-size:13px}' +
'.perrow__label{color:#6d766e}' +
'.perrow__select{font:inherit;font-size:13px;padding:2px 6px;border:1px solid #cbd2d9;' +
'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);' +
'transition:transform .16s ease,box-shadow .16s ease}' +
@ -467,24 +503,33 @@ useShadowStyles(
// the card's outline. Horizontal overflow traded for vertical. min-width:0 alone lets the
// controls compress on ONE line, which is what the row was always meant to be.
// The gate that passed this only measured horizontal edges; it now measures the row's top too.
'.pfs{position:absolute;bottom:-14px;left:50%;transform:translateX(-50%);' +
// ⚠ ANCHORED AT THE TOP, NOT THE BOTTOM. This was bottom:-14px, so any growth pushed the row
// UPWARD over the preview — which is exactly how wrapping it turned a sideways overflow into
// 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.
'.pfs{position:absolute;top:100%;margin-top:-14px;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
// correct in source for so long and the row still burst the card.
'width:82%;max-width:82%;min-width:0;box-sizing:border-box;margin:0;padding:0 4px 3px;' +
'border:1px solid rgba(0,0,0,.14);' +
'border-radius:2px;background:#fff;display:flex;flex-wrap:nowrap;gap:2px;justify-content:center;' +
'border-radius:2px;background:#fff;display:flex;flex-wrap:wrap;gap:2px;justify-content:center;' +
'box-shadow:0 1px 4px rgba(0,0,0,.10)}' +
// Let every control shrink inside the row rather than push it open. overflow:hidden +
// text-overflow keeps a squeezed label readable instead of spilling.
'.pfs > .pbtn,.pfs > .pstock{min-width:0;max-width:100%;overflow:hidden;' +
'text-overflow:ellipsis;white-space:nowrap}' +
// ⚠ NO ELLIPSIS CRUSH. min-width:0 alone let three 76-79px labels share 172px — 40px each,
// about five characters, so 'Prayer List' and 'Poster Map' both read 'Pra…'. MEASURED: 81
// 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:1 1 0;max-width:38%}' +
'.pfs > .pstock{flex:0 1 auto;min-width:0;max-width:100%}' +
'.is-dark .pfs{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}' +
@ -621,7 +666,19 @@ useShadowStyles(
</span>
</component>
<div v-show="!isCollapsed(sec.name)" class="grid">
<!-- ★ THE PER-ROW CONTROL. A <select>, not a stepper or a pair of chevrons: it is the control
every reader already knows for "choose one of a few", it is keyboard- and
screen-reader-operable with no work, and it shows the current value without a second
element. Imitating the solved pattern rather than inventing one.
Rendered once per section, beside the grid it governs, and only where the caller asked
for it — so neither existing pane grows a knob it did not request. -->
<div v-if="columnsControl && !isCollapsed(sec.name)" class="perrow">
<label class="perrow__label" :for="`perrow-${instanceId}-${sec.name}`">{{ t('buttons.postersPerRow') }}</label>
<select :id="`perrow-${instanceId}-${sec.name}`" class="perrow__select" v-model.number="perRow">
<option v-for="n in PER_ROW_CHOICES" :key="n" :value="n">{{ n }}</option>
</select>
</div>
<div v-show="!isCollapsed(sec.name)" class="grid" :style="gridStyle">
<div v-for="item in sec.items" :key="item.id" class="card"
:class="{ 'is-last': lastShown === item.id }" :data-list-id="item.id">

View file

@ -41,5 +41,6 @@
],
"clearSelection": "إلغاء التحديد",
"noOptions": "لا توجد نتائج",
"renderingLocal": "يتم إنشاء صورة بجودة الطباعة في متصفحك — لا يتم تحميل أي شيء."
"renderingLocal": "يتم إنشاء صورة بجودة الطباعة في متصفحك — لا يتم تحميل أي شيء.",
"postersPerRow": "الملصقات في كل صف"
}

View file

@ -41,5 +41,6 @@
],
"clearSelection": "Auswahl aufheben",
"noOptions": "Keine Optionen gefunden",
"renderingLocal": "Ein druckfähiges Bild wird in deinem Browser erzeugt — es wird nichts hochgeladen."
"renderingLocal": "Ein druckfähiges Bild wird in deinem Browser erzeugt — es wird nichts hochgeladen.",
"postersPerRow": "Poster pro Zeile"
}

View file

@ -41,5 +41,6 @@
"previewPrintPoster": "Preview & print the {label} poster",
"clearSelection": "Clear selection",
"noOptions": "No options found",
"renderingLocal": "Rendering a print-quality image in your browser — nothing is uploaded."
"renderingLocal": "Rendering a print-quality image in your browser — nothing is uploaded.",
"postersPerRow": "Posters per row"
}

View file

@ -41,5 +41,6 @@
],
"clearSelection": "Borrar la selección",
"noOptions": "No se encontraron opciones",
"renderingLocal": "Se está generando una imagen con calidad de impresión en tu navegador: no se sube nada."
"renderingLocal": "Se está generando una imagen con calidad de impresión en tu navegador: no se sube nada.",
"postersPerRow": "Pósteres por fila"
}

View file

@ -41,5 +41,6 @@
],
"clearSelection": "Effacer la sélection",
"noOptions": "Aucune option trouvée",
"renderingLocal": "Une image de qualité impression est générée dans votre navigateur — rien n’est envoyé."
"renderingLocal": "Une image de qualité impression est générée dans votre navigateur — rien n’est envoyé.",
"postersPerRow": "Affiches par ligne"
}

View file

@ -41,5 +41,6 @@
],
"clearSelection": "चयन हटाएँ",
"noOptions": "कोई विकल्प नहीं मिला",
"renderingLocal": "आपके ब्राउज़र में प्रिंट-गुणवत्ता वाली छवि बन रही है — कुछ भी अपलोड नहीं किया जाता।"
"renderingLocal": "आपके ब्राउज़र में प्रिंट-गुणवत्ता वाली छवि बन रही है — कुछ भी अपलोड नहीं किया जाता।",
"postersPerRow": "प्रति पंक्ति पोस्टर"
}

View file

@ -41,5 +41,6 @@
],
"clearSelection": "Cancella la selezione",
"noOptions": "Nessuna opzione trovata",
"renderingLocal": "Un’immagine con qualità di stampa viene generata nel tuo browser: nulla viene caricato."
"renderingLocal": "Un’immagine con qualità di stampa viene generata nel tuo browser: nulla viene caricato.",
"postersPerRow": "Poster per riga"
}

View file

@ -41,5 +41,6 @@
],
"clearSelection": "Limpar a seleção",
"noOptions": "Nenhuma opção encontrada",
"renderingLocal": "Uma imagem com qualidade de impressão está a ser gerada no seu navegador — nada é enviado."
"renderingLocal": "Uma imagem com qualidade de impressão está a ser gerada no seu navegador — nada é enviado.",
"postersPerRow": "Cartazes por linha"
}

View file

@ -41,5 +41,6 @@
],
"clearSelection": "Șterge selecția",
"noOptions": "Nicio opțiune găsită",
"renderingLocal": "O imagine de calitate pentru tipar este generată în browserul tău — nimic nu este încărcat."
"renderingLocal": "O imagine de calitate pentru tipar este generată în browserul tău — nimic nu este încărcat.",
"postersPerRow": "Postere pe rând"
}

View file

@ -41,5 +41,6 @@
],
"clearSelection": "Снять выделение",
"noOptions": "Ничего не найдено",
"renderingLocal": "Изображение типографского качества создаётся в вашем браузере — ничего не загружается на сервер."
"renderingLocal": "Изображение типографского качества создаётся в вашем браузере — ничего не загружается на сервер.",
"postersPerRow": "Плакатов в ряду"
}

View file

@ -41,5 +41,6 @@
],
"clearSelection": "清除选择",
"noOptions": "未找到选项",
"renderingLocal": "正在您的浏览器中生成可打印质量的图片——不会上传任何内容。"
"renderingLocal": "正在您的浏览器中生成可打印质量的图片——不会上传任何内容。",
"postersPerRow": "每行海报数"
}

View file

@ -515,8 +515,21 @@ useShadowStyles(
// Driver kept reporting. One host cannot be fixed by breaking the other, so the offset is
// conditional on the pill EXISTING: default raised to 8px (matching .stl-panel's own left
// inset), pushed down only when a rendered geocoder is actually in the tree.
'.stl-panel{top:8px!important}' +
'.upg-root:has(.mapboxgl-ctrl-geocoder) .stl-panel{top:58px!important}' +
// ★ SET --stl-top, DO NOT SET top DIRECTLY. The library caps the panel at
// calc(100% - var(--stl-top, 60px) - 12px) (SemanticTreeLegend.vue:605), so the offset and the
// height cap MUST come from one value. Overriding top alone moved the panel down while the cap
// kept subtracting the OLD offset, and the two silently disagreed.
// MEASURED 2026-09-24, upg-100-list hosted in a 420px-tall frame at 390px wide: top resolved to
// 58px while max-height resolved to calc(100% - 20px), so the panel ran to 458px in a 420px map
// and HUNG 38px PAST THE BOTTOM OF THE MAP — exactly 58 - 20. Same 38px at a 560px frame, because
// the error is the offset mismatch, not the map's size. Driver 2026-09-24: "the upg 100 list
// legend on the map is overflowing beyond the height of the map".
// ⚠ WHY THIS IS INVISIBLE ON EVERY PUBLISHED LEAF: a standalone leaf gives the map the FULL
// viewport, and a panel that overflows a full-height map is still inside the screen. It only
// shows when a host makes the map SHORTER than the viewport, which is how the element is really
// embedded. Verify in a constrained host, never on the bare leaf.
'.stl-panel{--stl-top:8px!important}' +
'.upg-root:has(.mapboxgl-ctrl-geocoder) .stl-panel{--stl-top:58px!important}' +
// Geocoder suggestion metadata layout (clone — bold field labels for PG rows).
'.dg-main{font:13px/1.3 ui-monospace,monospace}' +
'.dg-meta{margin-top:2px;font-size:11px;color:#475569;display:flex;flex-wrap:wrap;gap:8px}' +

View file

@ -669,6 +669,15 @@ useShadowStyles(
and the print UX here must be identical. deliverPoster() branches on that one
prop and routes BOTH the shipped-.webp and the browser-rasterised path through
openPrintFrame, so this is configuration — not a second implementation. -->
<!-- ★ TWO PER ROW, AND A KNOB. The Driver, verbatim: "i need two posters per row on
the regiosn posters and witha setitn got icnreat eh number of poswers per row".
The wall's own default is an auto-fill grid at minmax(210px,1fr), which lays out
five across a region card; two is a deliberate, larger card, and the control lets
a reader put it back.
Both props are OPT-IN on the shared component, so country-posters and
my-upg-100-list keep the auto-fill they have always had.
The comment sits ABOVE the tag because an HTML comment inside an attribute list is
a parse error — which is how the first attempt at this failed. -->
<PosterWall
:items="postersFor(region)"
:group-by="posterGroupBy"
@ -683,6 +692,8 @@ useShadowStyles(
:collapsible="false"
:embedded="true"
:section-heads="false"
:columns="2"
:columns-control="true"
:unsupported="posterUnsupported"
download-prefix="doxa-country"
poster-action="print"

View file

@ -412,6 +412,9 @@ function applySubject() {
// frame. The DRIVER RULED AGAINST IT (2026-09-24): the maps are a clone and "no change other
// than color and scroll side bar", the globe included. Deleted rather than argued — and
// deleting is the right shape of fix here, since their page sets no projection at all.
// Expose the box we asked to fit, so a probe can ask the engine what IT would have chosen for
// the same box and tell 'the fit is wrong' apart from 'something overrode the fit'.
if (props.bounds && typeof window !== 'undefined') window.__rcmBounds = props.bounds
if (props.bounds) m.fitBounds(props.bounds, { padding: 40, maxZoom: 6, duration: 600 })
else m.flyTo({ center: props.center, zoom: chooserZoom(m), duration: 600 })
}

View file

@ -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-urEju/yDTxk2w6K/Q47C3j1Br2UfZSats0Nw7WT+GTQjgQDYmr11XCCZNDaDMfn7" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-8fQR1xHoPidgeI7VU/Z82bxA303lsM/UZXfsgRu8MZG/Z5ASzgiel/am7dHiO2kH" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-urEju/yDTxk2w6K/Q47C3j1Br2UfZSats0Nw7WT+GTQjgQDYmr11XCCZNDaDMfn7" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-8fQR1xHoPidgeI7VU/Z82bxA303lsM/UZXfsgRu8MZG/Z5ASzgiel/am7dHiO2kH" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-urEju/yDTxk2w6K/Q47C3j1Br2UfZSats0Nw7WT+GTQjgQDYmr11XCCZNDaDMfn7" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-8fQR1xHoPidgeI7VU/Z82bxA303lsM/UZXfsgRu8MZG/Z5ASzgiel/am7dHiO2kH" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -158,7 +158,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "doxa-maps/country-posters/country-posters.js",
"integrity": "sha384-urEju/yDTxk2w6K/Q47C3j1Br2UfZSats0Nw7WT+GTQjgQDYmr11XCCZNDaDMfn7",
"integrity": "sha384-8fQR1xHoPidgeI7VU/Z82bxA303lsM/UZXfsgRu8MZG/Z5ASzgiel/am7dHiO2kH",
"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-urEju/yDTxk2w6K/Q47C3j1Br2UfZSats0Nw7WT+GTQjgQDYmr11XCCZNDaDMfn7\" 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-8fQR1xHoPidgeI7VU/Z82bxA303lsM/UZXfsgRu8MZG/Z5ASzgiel/am7dHiO2kH\" 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-urEju/yDTxk2w6K/Q47C3j1Br2UfZSats0Nw7WT+GTQjgQDYmr11XCCZNDaDMfn7\" 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-8fQR1xHoPidgeI7VU/Z82bxA303lsM/UZXfsgRu8MZG/Z5ASzgiel/am7dHiO2kH\" 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-urEju/yDTxk2w6K/Q47C3j1Br2UfZSats0Nw7WT+GTQjgQDYmr11XCCZNDaDMfn7\" 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-8fQR1xHoPidgeI7VU/Z82bxA303lsM/UZXfsgRu8MZG/Z5ASzgiel/am7dHiO2kH\" 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>"
}
}
]
@ -596,7 +596,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "doxa-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY",
"integrity": "sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge",
"elements": [
"gospel-resources-map"
],
@ -616,7 +616,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -630,7 +630,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -644,7 +644,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -658,7 +658,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -672,7 +672,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Efc6hgd36i6v0HtBZX+Y8Arw4U+rasZP7JTnS4Hnt1s8MMKLF3Ds3uLjWLy+z0FY\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-8UEOnYeQjLWBgc3P5zyoMIifZM5k/vOhFWUXzLAaJWoYE66hGZ21nXf3rRPpRWge\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
}
]
@ -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-4QjITklNF8dldIMyzCtJ89iM0orN98L0CNHUbBo04Vtis/xxe1qkkEFQkCF/04i7",
"integrity": "sha384-hBqkpEcVb+g3Pbin8O8dlRGJGUTfOGUoU5zCCQhr7wwWkDafVraBZCQfBItxRKcI",
"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-4QjITklNF8dldIMyzCtJ89iM0orN98L0CNHUbBo04Vtis/xxe1qkkEFQkCF/04i7\" 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-hBqkpEcVb+g3Pbin8O8dlRGJGUTfOGUoU5zCCQhr7wwWkDafVraBZCQfBItxRKcI\" 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-4QjITklNF8dldIMyzCtJ89iM0orN98L0CNHUbBo04Vtis/xxe1qkkEFQkCF/04i7\" 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-hBqkpEcVb+g3Pbin8O8dlRGJGUTfOGUoU5zCCQhr7wwWkDafVraBZCQfBItxRKcI\" 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-4QjITklNF8dldIMyzCtJ89iM0orN98L0CNHUbBo04Vtis/xxe1qkkEFQkCF/04i7\" 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-hBqkpEcVb+g3Pbin8O8dlRGJGUTfOGUoU5zCCQhr7wwWkDafVraBZCQfBItxRKcI\" 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-s3AcTJ/l3nqLOee2cxAVz2t+CxUG+5HmuLqZLf+Zxh4qF7bUNge8I7x73rMuy+oY",
"integrity": "sha384-Jvx1V3YrtL3L0p6eqByA/1hw4IWz6NpLp7Uza6fbl3HZCVa7JiAR5WMFvYsN6tPP",
"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-s3AcTJ/l3nqLOee2cxAVz2t+CxUG+5HmuLqZLf+Zxh4qF7bUNge8I7x73rMuy+oY\" 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-Jvx1V3YrtL3L0p6eqByA/1hw4IWz6NpLp7Uza6fbl3HZCVa7JiAR5WMFvYsN6tPP\" 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-s3AcTJ/l3nqLOee2cxAVz2t+CxUG+5HmuLqZLf+Zxh4qF7bUNge8I7x73rMuy+oY\" 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-Jvx1V3YrtL3L0p6eqByA/1hw4IWz6NpLp7Uza6fbl3HZCVa7JiAR5WMFvYsN6tPP\" 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-s3AcTJ/l3nqLOee2cxAVz2t+CxUG+5HmuLqZLf+Zxh4qF7bUNge8I7x73rMuy+oY\" 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-Jvx1V3YrtL3L0p6eqByA/1hw4IWz6NpLp7Uza6fbl3HZCVa7JiAR5WMFvYsN6tPP\" 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>"
}
}
]

File diff suppressed because one or more lines are too long

View file

@ -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-4QjITklNF8dldIMyzCtJ89iM0orN98L0CNHUbBo04Vtis/xxe1qkkEFQkCF/04i7" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-hBqkpEcVb+g3Pbin8O8dlRGJGUTfOGUoU5zCCQhr7wwWkDafVraBZCQfBItxRKcI" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-4QjITklNF8dldIMyzCtJ89iM0orN98L0CNHUbBo04Vtis/xxe1qkkEFQkCF/04i7" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-hBqkpEcVb+g3Pbin8O8dlRGJGUTfOGUoU5zCCQhr7wwWkDafVraBZCQfBItxRKcI" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-4QjITklNF8dldIMyzCtJ89iM0orN98L0CNHUbBo04Vtis/xxe1qkkEFQkCF/04i7" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-hBqkpEcVb+g3Pbin8O8dlRGJGUTfOGUoU5zCCQhr7wwWkDafVraBZCQfBItxRKcI" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-s3AcTJ/l3nqLOee2cxAVz2t+CxUG+5HmuLqZLf+Zxh4qF7bUNge8I7x73rMuy+oY" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-Jvx1V3YrtL3L0p6eqByA/1hw4IWz6NpLp7Uza6fbl3HZCVa7JiAR5WMFvYsN6tPP" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -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-s3AcTJ/l3nqLOee2cxAVz2t+CxUG+5HmuLqZLf+Zxh4qF7bUNge8I7x73rMuy+oY" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-Jvx1V3YrtL3L0p6eqByA/1hw4IWz6NpLp7Uza6fbl3HZCVa7JiAR5WMFvYsN6tPP" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-s3AcTJ/l3nqLOee2cxAVz2t+CxUG+5HmuLqZLf+Zxh4qF7bUNge8I7x73rMuy+oY" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-Jvx1V3YrtL3L0p6eqByA/1hw4IWz6NpLp7Uza6fbl3HZCVa7JiAR5WMFvYsN6tPP" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: