Merge remote-tracking branch 'origin/development' into repo/merge-main-bless

This commit is contained in:
Doxa Maps 2026-10-01 00:54:28 +00:00
commit fe43c966f3
139 changed files with 3945 additions and 641 deletions

View file

@ -42,7 +42,7 @@ const refuse = (msg) => (PUBLIC
// fetch() hands back the body DECOMPRESSED, so the upstream Content-Encoding and Content-Length no longer describe
// what is sent on: forwarding them made browsers try to gunzip plain HTML, and PocketBase's dashboard never rendered
// (rega, 2026-09-29). And PocketBase redirects to its own root (/_ -> /_/), which is put back under the mount (/pb/_/).
// (measured 2026-09-29). And PocketBase redirects to its own root (/_ -> /_/), which is put back under the mount (/pb/_/).
// A backend path as the backend will read it: percent-decoded, runs of slashes collapsed. The admin gates run on
// THIS form, so /pb/%5F/ (PocketBase decodes it to /_/) meets the same check as /pb/_/ (studio, 2026-09-29).
// Encoded slashes or backslashes and undecodable input are refused: nothing legitimate sends them.
@ -179,7 +179,7 @@ Bun.serve({
const bp = backendPath(p)
if (bp === null) return text('bad path', 400)
if (!p.startsWith('/api-prod/api/') || !bp.startsWith('/api-prod/api/')) return text('only /api/ is public', 403)
// As on /pb: the superuser and every _system collection stay closed. No public client uses them (dx-base, V2 review).
// As on /pb: the superuser and every _system collection stay closed. No public client uses them (a platform security review).
if (bp.startsWith('/api-prod/api/collections/_')) return text('for admins only', 403)
return proxy(req, url, '/api-prod/', 8091)
}

View file

@ -13,7 +13,7 @@
> bundle `parameterized` (cap-2) no matter how light it looks, and a `meta.json` `type`
> declares tiers 3/4/5.
Generated from 26 bundle(s).
Generated from 27 bundle(s).
### One canonical exemplar per tier (generated — quote these)
@ -51,6 +51,8 @@ Generated from 26 bundle(s).
| `gospel-resources` | `resources-map` | `single` |
| `my-upg-100-list` | `poster-wall` | `single` |
| `my-upg-100-list` | `upg-100-list` | `single` |
| `poster-design` | `poster-sizes` | `single` |
| `poster-design` | `poster-styles` | `single` |
| `prayer-walk-planner` | `prayer-walk-planner` | `single` |
| `regions-dashboard` | `region-dashboards` | `single` |
| `regions-dashboard` | `regions-list` | `single` |
@ -83,6 +85,7 @@ Generated from 26 bundle(s).
| `dashboard-plus-grid` | `dashboard-plus-grid` | `nested-elements-mfe` |
| `gospel-resources` | `gospel-dashboard` | `nested-elements-mfe` |
| `my-upg-100-list` | `upg-100-mfe` | `nested-elements-mfe` |
| `poster-design` | `poster-design-mfe` | `nested-elements-mfe` |
| `regions-dashboard` | `regions-mfe` | `nested-elements-mfe` |
| `research-map-plus-grid` | `research-map-plus-grid` | `nested-elements-mfe` |
| `stack-demo` | `stack-demo` | `nested-elements-mfe` |

View file

@ -126,6 +126,7 @@ const LINKS = [
// Composites whose children deliberately exchange nothing. The reason is printed on every run.
const NO_LINKS = {
'stack-demo': 'a demonstration stack: page knobs only; dashboard-plus-grid is the linked version',
'poster-design': 'two independent views of the poster templates (Styles, Sizes); neither pane drives the other',
}
// ── D: late getCurrentInstance() sites already routed to an owner ─────────────────────────────

View file

@ -25,9 +25,9 @@
* owner; counting it as a pass would be false. It exits 75 NOT MEASURED instead, NAMING every
* distinct blocked request path and the token's source, so the next reader can tell which
* credential was in play without the gate ever printing it.
* ✗ AN EARLIER VERSION CALLED THIS "TILESET ENTITLEMENT" AND THAT WAS WRONG. Measured 2026-09-29 by
* rega on this gate's own token: /v4/ streets-v8 and country-boundaries both return 200, with no
* Referer and from research.nuxtbase.app, maps-dev.nuxtbase.app, doxa.life, 127.0.0.1 and
* ✗ AN EARLIER VERSION CALLED THIS "TILESET ENTITLEMENT" AND THAT WAS WRONG. Measured 2026-09-29 with
* this gate's own token: /v4/ streets-v8 and country-boundaries both return 200, with no
* Referer and from our research and maps-dev hosts, doxa.life, 127.0.0.1 and
* example.com. The token is unrestricted and entitled. The old claim rested on three no-Referer
* probes, which cannot distinguish a URL-restricted token from a missing entitlement. Whatever 403s
* is a different request or a different token — which is exactly what the gate must now report
@ -102,9 +102,9 @@ async function serve(dir) {
// rather than FAILED.
//
// ✗ WHY THIS NO LONGER CLAIMS "ENTITLEMENT" (corrected 2026-09-29). It used to report a 403 here
// as "tileset, not the embed", on the strength of three no-Referer probes. MEASURED since, by rega,
// as "tileset, not the embed", on the strength of three no-Referer probes. MEASURED since,
// with the very token this gate runs on: /v4/ streets-v8 AND country-boundaries return 200 — with
// no Referer, and with research.nuxtbase.app, maps-dev.nuxtbase.app, doxa.life, 127.0.0.1 and
// no Referer, and with our research and maps-dev hosts, doxa.life, 127.0.0.1 and
// example.com. The token is unrestricted and entitled, so whatever 403s here is a DIFFERENT
// request (a style, sprite, glyphs, a composite with terrain) or a different token than the one
// tested. Three no-Referer cases cannot tell a URL-restricted token from a missing entitlement,
@ -168,7 +168,7 @@ async function measure(page, url, provider) {
: c.classList.contains('maplibregl-canvas') ? 'maplibre' : 'other'
// ★ EVERY map, not the two this repo happens to publish. Reading only
// window.__rcmMap/__rdashMap made 5 of the 7 production leaves report -1 and then FAIL as
// "0 rendered features" even when they had drawn (measured by rega, 2026-09-29). The hook
// "0 rendered features" even when they had drawn (measured 2026-09-29). The hook
// installed before navigation collects every Map the page constructs, whatever it calls it.
const maps = (window.__gateMaps || []).slice()
const published = window.__rcmMap || window.__rdashMap || null

View file

@ -63,8 +63,19 @@ const HAND_VOCAB = {
'Project 700',
'How this list was found',
],
// The phrases that NAME a series. Derived vocabulary drops any sentence two series share, so a verbatim
// copy of one series' framing into another would go unseen without these (the copy becomes "shared").
country: [
'PRAY FOR THIS COUNTRY',
'Country Prayer Map',
'one sheet for each country',
'which country God has put',
],
indiaState: [
'PRAY FOR THIS STATE',
'State Prayer Map',
'one sheet for each state',
'which state God has put',
],
}
@ -100,13 +111,13 @@ function deriveVocab(series) {
...strings(e.poster || {}),
].map((s) => s.trim()).filter((s) => s.length >= 12))
}
// Drop anything two series share — it cannot identify either of them.
for (const a of ids) {
for (const b of ids) {
if (a === b) continue
for (const s of own[b]) own[a].delete(s)
}
}
// Drop anything two series share — it cannot identify either of them. The shared set is found on the
// ORIGINAL sets first: deleting while still comparing pairs removed a shared string from the earlier
// series only, so the later one kept it as a "fingerprint" and the earlier one failed for carrying it.
// Invisible with two series; a third (indiaState, sharing country's networks) exposed it.
const shared = new Set()
for (const a of ids) for (const b of ids) if (a !== b) for (const s of own[a]) if (own[b].has(s)) shared.add(s)
for (const id of ids) for (const s of shared) own[id].delete(s)
const out = {}
for (const id of ids) out[id] = [...new Set([...(HAND_VOCAB[id] || []), ...own[id]])]
return out
@ -120,8 +131,10 @@ function* strings(v) {
}
/** The text a series OWNS: its overrides and its poster furniture. Not its prose documentation. */
// The poster header block too: that is where the country poster once printed the UPG-100 side box, and a scan
// of strings + networks alone could never see a series' framing copied into another's poster text.
function seriesText(entry) {
return [...strings(entry.strings || {}), ...strings(entry.networks || [])]
return [...strings(entry.strings || {}), ...strings(entry.networks || []), ...strings(entry.poster || {})]
}
function checkDeclarations(series) {
@ -208,6 +221,9 @@ function selfTest() {
['upg100 loses its funnel', (s) => { s.upg100.funnel = false }],
['a series loses a networks block', (s) => { s.country.networks.pop() }],
['dataDir points nowhere', (s) => { s.country.dataDir = 'no/such/folder' }],
['indiaState keeps the country side box', (s) => { s.indiaState.poster.sideBoxHeading = 'What is a Country Prayer Map?' }],
['country keeps the state network line', (s) => { s.country.networks[0][1] = s.indiaState.networks[0][1] }],
['indiaState keeps PRAY FOR THIS COUNTRY', (s) => { s.indiaState.strings.map_page_kicker = 'PRAY FOR THIS COUNTRY' }],
]
let pass = 0
for (const [name, mutate] of cases) {
@ -224,6 +240,19 @@ function selfTest() {
fails.length = 0
if (pass !== cases.length) fail(`self-test: caught ${pass}/${cases.length} known-bad inputs`)
else ok(`self-test: caught all ${cases.length} known-bad inputs`)
// Shared wording is nobody's fingerprint, in EITHER order: three series where the first and last share a
// sentence must neither flag it nor lose their own phrases.
const SHARED = 'A sentence two series both print, word for word.'
const tri = {
a: { strings: { k: 'Only series a says this phrase.' }, networks: [['h', SHARED]] },
b: { strings: { k: 'Only series b says this phrase.' }, networks: [] },
c: { strings: { k: 'Only series c says this phrase.' }, networks: [['h', SHARED]] },
}
const v = deriveVocab(tri)
const leaked = Object.entries(v).filter(([, p]) => p.includes(SHARED)).map(([id]) => id)
const lost = ['a', 'b', 'c'].filter((id) => !v[id].includes(`Only series ${id} says this phrase.`))
if (leaked.length || lost.length) fail(`self-test: shared wording kept as vocabulary of [${leaked}] / own phrases lost for [${lost}]`)
else ok('self-test: shared wording is dropped from every series, whatever their order')
}
// ── run ──────────────────────────────────────────────────────────────────────

View file

@ -167,13 +167,18 @@ const run = (rule, src) => rule.test ? rule.test(src) : [...src.matchAll(rule.re
const lineOf = (src, idx) => src.slice(0, idx).split('\n').length
// ── self-test: every rule must see a planted violation ─────────────────────────────────────────
// The TOK-LITERAL sample is ASSEMBLED HERE, never written as one literal: a committed string shaped like a
// real Mapbox token is itself what that rule forbids, and reads as a leaked credential to any secret scan.
// It is plainly fake ('example, not a token'), yet shaped like one: pk. + base64url JSON + . + a tail, which
// is all the rule's regex looks for, so the self-test still proves the rule fires.
const FAKE_TOKEN = 'pk.' + Buffer.from('{"u":"example","note":"not-a-token"}').toString('base64url') + '.' + 'x'.repeat(22)
const PLANTED = `
map.setStyle(isDark ? 'mapbox://styles/mapbox/dark-v11' : 'x')
const m2 = new gl.Map({ container: el, style: 'mapbox://styles/mapbox/light-v11' })
const p = new mapboxgl.Popup()
useMapInstance({ container: el, provider: 'mapbox' })
const tk = window.DOXA_TOKEN
const t = 'pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJjbGFudGVkIn0'
const t = '${FAKE_TOKEN}'
n.toLocaleString('en-US')
const r = await fetch(base + '/api/people-groups/list?fields=slug')
const dsm = new DataSourceManager({ locale: 'en' })

View file

@ -77,7 +77,7 @@ function readBuildConfig() {
// EMBED_BASE_URL overrides the committed embedBaseUrl, the same way DOXA_SYNC_PATHS extends paths:
// a host that builds its own tree (maps-dev's /cdn build inherits the container env) bakes absolute
// snippet URLs without a committed config change. It is the base WITHOUT the tree name
// (https://maps-dev.nuxtbase.app/cdn -> .../cdn/<name>/...). check-embed-targets-exist reads the same.
// (https://<host>/cdn -> .../cdn/<name>/...). check-embed-targets-exist reads the same.
const embedBaseUrl = ((process.env.EMBED_BASE_URL || '').trim()
|| (typeof cfg.embedBaseUrl === 'string' ? cfg.embedBaseUrl.trim() : '')).replace(/\/+$/, '')
const tokenUrl = typeof cfg.tokenUrl === 'string' ? cfg.tokenUrl.trim() : ''
@ -857,9 +857,9 @@ const MAPLIBRE_STYLE_ORIGIN = 'https://tiles.openfreemap.org' // useMapInstanc
* token; the hint follows the build default and the report names that miss case (one unused preconnect).
*/
function engineFor({ config }) {
// CORRECTED 2026-09-29 (regions executed the shipping resolver; rega's A9NET confirmed on the wire):
// CORRECTED 2026-09-29 (the shipping resolver was executed, and a network trace confirmed it on the wire):
// a token no longer selects Mapbox. useMapProvider's default is MapLibre unless provider is EXPLICITLY
// 'mapbox'. rega measured all 8 auto leaves — which DO carry a pray.doxa.life token endpoint — loading
// 'mapbox'. A browser check measured all 8 auto leaves — which DO carry a pray.doxa.life token endpoint — loading
// MapLibre from jsdelivr cold. So the hint's engine is Mapbox ONLY when provider:'mapbox' is set; every
// other case (auto, or provider:'maplibre') is MapLibre, regardless of any baked token / tokenUrl.
return String((config && config.provider) || '').toLowerCase().trim() === 'mapbox' ? 'mapbox' : 'maplibre'
@ -886,7 +886,7 @@ function resourceHints({ isMapLibre, config, apiBaseUrl }) {
const gl = engine === 'maplibre' ? CDN.maplibreGlJs : CDN.mapboxGlJs
lines.push(` <link rel="preload" as="script" href="${gl.url}" integrity="${gl.integrity}" crossorigin="anonymous" />`)
}
return lines.length ? ` <!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->\n${lines.join('\n')}\n` : ''
return lines.length ? ` <!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->\n${lines.join('\n')}\n` : ''
}
// Does this BUILT bundle .js reference a map engine? Read from public/<tree>/<bundle>/<bundle>.js with the
@ -957,14 +957,14 @@ function mapPageHtml(opts) {
// EVERY non-map leaf (cap-4 composites + cap-1/2/3 non-map leaves), not just cap-5 shells.
const shellOnly = !!shellTokenFree || notAMap
const tokenFree = isMapLibre || shellOnly
// A9 leaf hints (dx-base/a9-leaf-hints): give the browser the map's origins BEFORE the bundle
// A9 leaf hints: give the browser the map's origins BEFORE the bundle
// parses. Engine-aware, derived from the SAME inputs the runtime ladder reads (below), never
// hardcoded: preconnect the token-implied engine + the style host + the data origin, and — only
// where the leaf emits no engine <script> today (the 'auto' leaf) — preload the engine, its URL,
// integrity and crossorigin from library/constants/cdn.js so it cannot drift from the runtime tag.
// WHO GETS HINTS: any page whose OWN bundle loads a map engine — the directly verifiable signal
// (`bundleLoadsEngine`, an engine reference in the built .js), NOT the leaf's `isMap` flag. rega's A9
// net run found regions-mfe (isMap:false, 56 engine refs in its bundle) AND research-map-plus-grid
// (`bundleLoadsEngine`, an engine reference in the built .js), NOT the leaf's `isMap` flag. A network
// run found regions-mfe (isMap:false, 56 engine refs in its bundle) AND research-map-plus-grid
// (isMap:false, own bundle 0 refs but composes doxa-research-map, 74 refs) opening cdn.jsdelivr.net +
// tiles.openfreemap.org from the top page and getting no hints — my first cut keyed on isMap and missed
// both, the same trap check:leaf-engine-per-provider's isMap gate fell into. bundleLoadsEngine is true
@ -1061,7 +1061,7 @@ ${hintBlock} ${peerBlock}
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
@ -1310,7 +1310,7 @@ function instanceHtml(b, profile, inst, tokenUrlExpr, bakedToken) {
return mapPageHtml({
title: `${inst.title} · ${b.bundle}`,
tag,
// MERGE, never replace (go-int: a preset lost its profile's pinHint:false): the profile's
// MERGE, never replace (a preset once lost its profile's pinHint:false): the profile's
// <profile>.meta.json `config` first, then the instance's props over it. The rule is stated in
// documentation/PRESET-PLUGIN-STANDARD.md and asserted by check:preset-config-merge.
config: { assetsBase: assetsBaseFor(4), ...(profile.extraConfig || {}), ...mobileConfig(profile), ...inst.props },
@ -1724,7 +1724,7 @@ function writeManifestAndHtml(root, bundles, tokenUrlExpr, bakedToken) {
// THE FRONT PAGE IS THE STUDIO (see studioRootHtml). The staging-forge chooser that used to be
// written here (rootHtml) is retired with it; nothing is written above the tree any more.
// …with the studio bundle's integrity, written HERE: it used to be added only by fix:sri, so running
// `bun run tree` after fix:sri stripped it again (rega, 2026-09-25).
// `bun run tree` after fix:sri stripped it again (found 2026-09-25).
writeFileSync(join(root, 'index.html'), studioRootHtml(bakedToken, (bundles.find((b) => b.bundle === 'plugin-studio') || {}).integrity || ''))
@ -2019,7 +2019,7 @@ function main() {
}
// people-groups/lists/*.json — the ?list= / profile-config `list` prop fetches the chosen list by name
// (frontier-500k, frontier, unreached, unengaged), so the whole folder ships. Missing, every non-default
// list 404'd on the built tree and drew 0 pins (go-int / rega at a71007f1).
// list 404'd on the built tree and drew 0 pins (measured at a71007f1).
const listsSrc = join(pubAssets, 'people-groups', 'lists')
if (existsSync(listsSrc)) {
const listsDest = join(treeAssets, 'people-groups', 'lists')

View file

@ -2,7 +2,7 @@
* profile-meta.mjs — the per-profile fields a host reads from <profile>.meta.json, in ONE place.
*
* generate-tree (the built manifest.json) and the dev server's /__plugin-gallery/manifest.json both call
* this, so the dev Studio and the built Studio cannot disagree (rega: the corner widget never mounted in
* this, so the dev Studio and the built Studio cannot disagree (the corner widget once never mounted in
* dev because only the built manifest carried "widget").
* stage "production" | "experimental" default experimental
* heavy true when the element holds live WebGL contexts
@ -16,7 +16,7 @@ import { join } from 'node:path'
const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left']
/**
* ★ WHO ELSE MAY LIST THIS PLUGIN (dx-base, 2026-09-28). The platform dashboard reads each team's
* ★ WHO ELSE MAY LIST THIS PLUGIN (2026-09-28). The platform dashboard reads each team's
* own /cdn/dx-maps/manifest.json instead of one shared index, so this field is what decides whether
* a plugin appears in ANOTHER team's library (loaded read-only from the owner's CDN).
*

View file

@ -260,7 +260,7 @@ export function createAppI18n() {
// dir on .profile-loader from effectiveLocale (0f6d5049), so the plugin mirrors correctly
// whether or not it owns the document. This is only about the page around it.
//
// LANG TRAVELS WITH DIR (rega, research.nuxtbase.app/?lang=ar): an owned page flipped to dir=rtl but
// LANG TRAVELS WITH DIR (measured on a research page with ?lang=ar): an owned page flipped to dir=rtl but
// kept the generator's <html lang="en">, so a screen reader read the Arabic with an English voice (and
// ?lang=fr left lang="en" too). The page is ours, so its lang is the locale in force.
try {

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "الهند — الولايات والأقاليم الاتحادية",
"indiaStateNoGroups": "لا توجد هنا شعوب غير مُبشَّرة مُدرَجة."
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "عرض التفاصيل كاملة",
"showFullDetailsAria": "عرض التفاصيل كاملة لـ {label}",
"tab": {
"styles": "الأنماط",
"sizes": "المقاسات"
},
"styles": {
"heading": "أنماط الملصقات",
"intro": "كل قوالب الملصقات في مكان واحد، مرسومة من المكتبة ببيانات نموذجية. تحمل كل بطاقة اسم إعدادها، لتطلب من الوكيل هذا القالب بعينه."
},
"sizes": {
"heading": "مقاسات الملصقات",
"waiting": "سيظهر سُلّم المقاسات (الورق القياسي، و3 × 3 أقدام، و3 × 5 أقدام، و5.5 × 5.5 أقدام، و5 × 8 أقدام) هنا بعد نشر جدول المقاسات المشترك. سيُعرض كل مقاس كمعاينة طباعة، بالاتجاه الطولي فقط."
},
"family": {
"mapSheet": "صفحات الخرائط",
"packet": "حزمة الصلاة",
"cards": "بطاقات الصلاة",
"wall": "ألواح الجدار",
"mapPrint": "طباعة الخريطة"
},
"tpl": {
"mapSheetCountry": "صفحة خريطة الدولة",
"mapSheetPacketPage": "صفحة الخريطة في الحزمة",
"packetFull": "حزمة الصلاة، كاملة",
"packetLightweight": "حزمة الصلاة، مختصرة",
"cards": "بطاقات الصلاة",
"blessPanels": "ألواح جدار البركة",
"mapPrint": "طباعة الخريطة"
},
"missing": {
"packet": "لا يرسم المعرض الحزمة بعد. تذكر التفاصيل مولّدها ونسختها.",
"cards": "لا يرسم المعرض البطاقات بعد. تذكر التفاصيل المولّد ومقاس البطاقة.",
"bless": "محرّك هذا القالب ليس في المكتبة المشتركة بعد، لذا لا يستطيع المعرض رسمه.",
"mapPrint": "يطبع هذا القالب الخريطة الحية التي ينظر إليها القارئ، لذا ليست له صورة نموذجية."
},
"details": {
"configName": "اسم الإعداد",
"copyName": "نسخ",
"size": "المقاس",
"fileSize": "حجم الملف",
"fileSizeUnknown": "يُقاس عند رسم الملصق",
"settings": "الإعدادات",
"sample": "بيانات نموذجية",
"renderer": "المولّد",
"legacy": "مقاس قديم: ما زال يُطبع، لكن أداة اختيار الطباعة لم تعد تعرضه.",
"forAgents": "لاستخدام هذا القالب، قل للوكيل: استخدم الإعداد {name}."
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "Indien – Bundesstaaten und Unionsterritorien",
"indiaStateNoGroups": "Hier sind keine unerreichten Volksgruppen verzeichnet."
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "Alle Details anzeigen",
"showFullDetailsAria": "Alle Details zu {label} anzeigen",
"tab": {
"styles": "Stile",
"sizes": "Größen"
},
"styles": {
"heading": "Posterstile",
"intro": "Alle Postervorlagen an einem Ort, aus der Bibliothek mit Beispieldaten gezeichnet. Jede Karte trägt den Namen ihrer Konfiguration, damit Sie einen Agenten um genau diese bitten können."
},
"sizes": {
"heading": "Postergrößen",
"waiting": "Die Größenstufen (Standardpapier, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft und 5 × 8 ft) erscheinen hier, sobald die gemeinsame Größentabelle veröffentlicht ist. Jede Größe wird als Druckvorschau gezeigt, nur im Hochformat."
},
"family": {
"mapSheet": "Kartenblätter",
"packet": "Gebetsmappe",
"cards": "Gebetskarten",
"wall": "Wandtafeln",
"mapPrint": "Kartendruck"
},
"tpl": {
"mapSheetCountry": "Länder-Kartenblatt",
"mapSheetPacketPage": "Kartenseite der Mappe",
"packetFull": "Gebetsmappe, vollständig",
"packetLightweight": "Gebetsmappe, kompakt",
"cards": "Gebetskarten",
"blessPanels": "Tafeln der Segnen-Wand",
"mapPrint": "Kartendruck"
},
"missing": {
"packet": "Die Galerie zeichnet die Mappe noch nicht. Die Details nennen ihren Generator und ihre Variante.",
"cards": "Die Galerie zeichnet die Karten noch nicht. Die Details nennen den Generator und das Kartenformat.",
"bless": "Die Engine dieser Vorlage ist noch nicht in der gemeinsamen Bibliothek, deshalb kann die Galerie sie nicht zeichnen.",
"mapPrint": "Diese Vorlage druckt die Live-Karte, die der Leser gerade ansieht, und hat deshalb kein Beispielbild."
},
"details": {
"configName": "Name der Konfiguration",
"copyName": "Kopieren",
"size": "Größe",
"fileSize": "Dateigröße",
"fileSizeUnknown": "Wird gemessen, wenn das Poster gezeichnet wird",
"settings": "Einstellungen",
"sample": "Beispieldaten",
"renderer": "Generator",
"legacy": "Alte Größe: Sie wird noch gedruckt, aber die Druckauswahl bietet sie nicht mehr an.",
"forAgents": "Um diese Vorlage zu verwenden, sagen Sie einem Agenten: verwende die Konfiguration {name}."
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "India — states and union territories",
"indiaStateNoGroups": "No unreached people groups are listed here."
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "Show full details",
"showFullDetailsAria": "Show full details for {label}",
"tab": {
"styles": "Styles",
"sizes": "Sizes"
},
"styles": {
"heading": "Poster styles",
"intro": "Every poster template in one place, drawn from the library with sample data. Each card is named by its configuration, so you can ask an agent for exactly that one."
},
"sizes": {
"heading": "Poster sizes",
"waiting": "The size ladder (standard paper, 3 × 3 ft, 3 × 5 ft, 5.5 × 5.5 ft and 5 × 8 ft) appears here once the shared size table is published. Each size will be shown as a print preview, portrait only."
},
"family": {
"mapSheet": "Map sheets",
"packet": "Prayer packet",
"cards": "Prayer cards",
"wall": "Wall panels",
"mapPrint": "Map print"
},
"tpl": {
"mapSheetCountry": "Country map sheet",
"mapSheetPacketPage": "Packet map page",
"packetFull": "Prayer packet, full",
"packetLightweight": "Prayer packet, lightweight",
"cards": "Prayer cards",
"blessPanels": "Bless wall panels",
"mapPrint": "Map print"
},
"missing": {
"packet": "The gallery does not draw the packet yet. The details name its renderer and variant.",
"cards": "The gallery does not draw the cards yet. The details name the renderer and the card stock.",
"bless": "This template's engine is not in the shared library yet, so the gallery cannot draw it.",
"mapPrint": "This template prints the live map the reader is looking at, so it has no sample picture."
},
"details": {
"configName": "Configuration name",
"copyName": "Copy",
"size": "Size",
"fileSize": "File size",
"fileSizeUnknown": "Measured when the poster is drawn",
"settings": "Settings",
"sample": "Sample data",
"renderer": "Renderer",
"legacy": "Legacy size: it still prints, but the print picker no longer offers it.",
"forAgents": "To use this template, tell an agent: use configuration {name}."
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "India: estados y territorios de la unión",
"indiaStateNoGroups": "Aquí no figura ningún pueblo no alcanzado."
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "Ver todos los detalles",
"showFullDetailsAria": "Ver todos los detalles de {label}",
"tab": {
"styles": "Estilos",
"sizes": "Tamaños"
},
"styles": {
"heading": "Estilos de póster",
"intro": "Todas las plantillas de póster en un solo lugar, dibujadas desde la biblioteca con datos de ejemplo. Cada tarjeta lleva el nombre de su configuración, para que puedas pedirle a un agente exactamente esa."
},
"sizes": {
"heading": "Tamaños de póster",
"waiting": "La escala de tamaños (papel estándar, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft y 5 × 8 ft) aparecerá aquí cuando se publique la tabla de tamaños compartida. Cada tamaño se mostrará como vista previa de impresión, solo en vertical."
},
"family": {
"mapSheet": "Hojas de mapa",
"packet": "Paquete de oración",
"cards": "Tarjetas de oración",
"wall": "Paneles de pared",
"mapPrint": "Impresión del mapa"
},
"tpl": {
"mapSheetCountry": "Hoja de mapa del país",
"mapSheetPacketPage": "Página de mapa del paquete",
"packetFull": "Paquete de oración, completo",
"packetLightweight": "Paquete de oración, ligero",
"cards": "Tarjetas de oración",
"blessPanels": "Paneles del muro Bendecir",
"mapPrint": "Impresión del mapa"
},
"missing": {
"packet": "La galería todavía no dibuja el paquete. Los detalles indican su generador y su variante.",
"cards": "La galería todavía no dibuja las tarjetas. Los detalles indican el generador y el tipo de tarjeta.",
"bless": "El motor de esta plantilla aún no está en la biblioteca compartida, así que la galería no puede dibujarla.",
"mapPrint": "Esta plantilla imprime el mapa en vivo que está mirando el lector, así que no tiene imagen de ejemplo."
},
"details": {
"configName": "Nombre de la configuración",
"copyName": "Copiar",
"size": "Tamaño",
"fileSize": "Tamaño del archivo",
"fileSizeUnknown": "Se mide al dibujar el póster",
"settings": "Ajustes",
"sample": "Datos de ejemplo",
"renderer": "Generador",
"legacy": "Tamaño heredado: todavía se imprime, pero el selector de impresión ya no lo ofrece.",
"forAgents": "Para usar esta plantilla, dile a un agente: usa la configuración {name}."
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "Inde — États et territoires de l’Union",
"indiaStateNoGroups": "Aucun peuple non atteint n’est répertorié ici."
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "Afficher tous les détails",
"showFullDetailsAria": "Afficher tous les détails de {label}",
"tab": {
"styles": "Styles",
"sizes": "Formats"
},
"styles": {
"heading": "Styles d'affiche",
"intro": "Tous les modèles d'affiche au même endroit, dessinés depuis la bibliothèque avec des données d'exemple. Chaque carte porte le nom de sa configuration, pour que vous puissiez demander exactement celle-ci à un agent."
},
"sizes": {
"heading": "Formats d'affiche",
"waiting": "L'échelle des formats (papier standard, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft et 5 × 8 ft) apparaîtra ici dès que la table des formats partagée sera publiée. Chaque format sera présenté en aperçu avant impression, en portrait uniquement."
},
"family": {
"mapSheet": "Feuilles de carte",
"packet": "Dossier de prière",
"cards": "Cartes de prière",
"wall": "Panneaux muraux",
"mapPrint": "Impression de la carte"
},
"tpl": {
"mapSheetCountry": "Feuille de carte du pays",
"mapSheetPacketPage": "Page de carte du dossier",
"packetFull": "Dossier de prière, complet",
"packetLightweight": "Dossier de prière, allégé",
"cards": "Cartes de prière",
"blessPanels": "Panneaux du mur Bénir",
"mapPrint": "Impression de la carte"
},
"missing": {
"packet": "La galerie ne dessine pas encore le dossier. Les détails indiquent son générateur et sa variante.",
"cards": "La galerie ne dessine pas encore les cartes. Les détails indiquent le générateur et le format de carte.",
"bless": "Le moteur de ce modèle n'est pas encore dans la bibliothèque partagée, la galerie ne peut donc pas le dessiner.",
"mapPrint": "Ce modèle imprime la carte en direct que le lecteur regarde, il n'a donc pas d'image d'exemple."
},
"details": {
"configName": "Nom de la configuration",
"copyName": "Copier",
"size": "Format",
"fileSize": "Taille du fichier",
"fileSizeUnknown": "Mesurée quand l'affiche est dessinée",
"settings": "Réglages",
"sample": "Données d'exemple",
"renderer": "Générateur",
"legacy": "Format ancien : il s'imprime encore, mais le sélecteur d'impression ne le propose plus.",
"forAgents": "Pour utiliser ce modèle, dites à un agent : utilise la configuration {name}."
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "भारत — राज्य और केंद्र शासित प्रदेश",
"indiaStateNoGroups": "यहाँ कोई अपहुँचा जनसमूह सूचीबद्ध नहीं है।"
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "पूरा विवरण दिखाएँ",
"showFullDetailsAria": "{label} का पूरा विवरण दिखाएँ",
"tab": {
"styles": "शैलियाँ",
"sizes": "आकार"
},
"styles": {
"heading": "पोस्टर शैलियाँ",
"intro": "सभी पोस्टर टेम्पलेट एक ही जगह, लाइब्रेरी से नमूना डेटा के साथ बनाए गए। हर कार्ड पर उसके कॉन्फ़िगरेशन का नाम है, ताकि आप किसी एजेंट से ठीक वही माँग सकें।"
},
"sizes": {
"heading": "पोस्टर आकार",
"waiting": "साझा आकार तालिका प्रकाशित होते ही आकारों की सीढ़ी (मानक कागज़, 3 × 3 फ़ुट, 3 × 5 फ़ुट, 5.5 × 5.5 फ़ुट और 5 × 8 फ़ुट) यहाँ दिखेगी। हर आकार प्रिंट पूर्वावलोकन के रूप में दिखेगा, केवल खड़े (पोर्ट्रेट) रूप में।"
},
"family": {
"mapSheet": "मानचित्र पृष्ठ",
"packet": "प्रार्थना पैकेट",
"cards": "प्रार्थना कार्ड",
"wall": "दीवार पैनल",
"mapPrint": "मानचित्र प्रिंट"
},
"tpl": {
"mapSheetCountry": "देश का मानचित्र पृष्ठ",
"mapSheetPacketPage": "पैकेट का मानचित्र पृष्ठ",
"packetFull": "प्रार्थना पैकेट, पूरा",
"packetLightweight": "प्रार्थना पैकेट, हल्का",
"cards": "प्रार्थना कार्ड",
"blessPanels": "आशीष दीवार के पैनल",
"mapPrint": "मानचित्र प्रिंट"
},
"missing": {
"packet": "गैलरी अभी पैकेट नहीं बनाती। विवरण में उसका जनरेटर और संस्करण बताया गया है।",
"cards": "गैलरी अभी कार्ड नहीं बनाती। विवरण में जनरेटर और कार्ड का आकार बताया गया है।",
"bless": "इस टेम्पलेट का इंजन अभी साझा लाइब्रेरी में नहीं है, इसलिए गैलरी इसे नहीं बना सकती।",
"mapPrint": "यह टेम्पलेट वह लाइव मानचित्र प्रिंट करता है जिसे पाठक देख रहा है, इसलिए इसकी कोई नमूना तस्वीर नहीं है।"
},
"details": {
"configName": "कॉन्फ़िगरेशन का नाम",
"copyName": "कॉपी करें",
"size": "आकार",
"fileSize": "फ़ाइल का आकार",
"fileSizeUnknown": "पोस्टर बनने पर मापा जाता है",
"settings": "सेटिंग्स",
"sample": "नमूना डेटा",
"renderer": "जनरेटर",
"legacy": "पुराना आकार: यह अब भी प्रिंट होता है, पर प्रिंट चयनकर्ता अब इसे नहीं दिखाता।",
"forAgents": "यह टेम्पलेट इस्तेमाल करने के लिए किसी एजेंट से कहें: कॉन्फ़िगरेशन {name} इस्तेमाल करो।"
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "India — stati e territori dell’Unione",
"indiaStateNoGroups": "Qui non è elencato alcun popolo non raggiunto."
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "Mostra tutti i dettagli",
"showFullDetailsAria": "Mostra tutti i dettagli di {label}",
"tab": {
"styles": "Stili",
"sizes": "Formati"
},
"styles": {
"heading": "Stili dei poster",
"intro": "Tutti i modelli di poster in un unico posto, disegnati dalla libreria con dati di esempio. Ogni scheda porta il nome della sua configurazione, così puoi chiedere a un agente esattamente quella."
},
"sizes": {
"heading": "Formati dei poster",
"waiting": "La scala dei formati (carta standard, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft e 5 × 8 ft) comparirà qui quando sarà pubblicata la tabella dei formati condivisa. Ogni formato sarà mostrato come anteprima di stampa, solo in verticale."
},
"family": {
"mapSheet": "Fogli mappa",
"packet": "Fascicolo di preghiera",
"cards": "Schede di preghiera",
"wall": "Pannelli da parete",
"mapPrint": "Stampa della mappa"
},
"tpl": {
"mapSheetCountry": "Foglio mappa del paese",
"mapSheetPacketPage": "Pagina mappa del fascicolo",
"packetFull": "Fascicolo di preghiera, completo",
"packetLightweight": "Fascicolo di preghiera, leggero",
"cards": "Schede di preghiera",
"blessPanels": "Pannelli del muro Benedici",
"mapPrint": "Stampa della mappa"
},
"missing": {
"packet": "La galleria non disegna ancora il fascicolo. I dettagli indicano il suo generatore e la variante.",
"cards": "La galleria non disegna ancora le schede. I dettagli indicano il generatore e il formato della scheda.",
"bless": "Il motore di questo modello non è ancora nella libreria condivisa, quindi la galleria non può disegnarlo.",
"mapPrint": "Questo modello stampa la mappa dal vivo che il lettore sta guardando, quindi non ha un'immagine di esempio."
},
"details": {
"configName": "Nome della configurazione",
"copyName": "Copia",
"size": "Formato",
"fileSize": "Dimensione del file",
"fileSizeUnknown": "Misurata quando il poster viene disegnato",
"settings": "Impostazioni",
"sample": "Dati di esempio",
"renderer": "Generatore",
"legacy": "Formato precedente: si stampa ancora, ma il selettore di stampa non lo offre più.",
"forAgents": "Per usare questo modello, di' a un agente: usa la configurazione {name}."
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "Índia — estados e territórios da união",
"indiaStateNoGroups": "Nenhum povo não alcançado está listado aqui."
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "Mostrar todos os detalhes",
"showFullDetailsAria": "Mostrar todos os detalhes de {label}",
"tab": {
"styles": "Estilos",
"sizes": "Tamanhos"
},
"styles": {
"heading": "Estilos de pôster",
"intro": "Todos os modelos de pôster num só lugar, desenhados a partir da biblioteca com dados de exemplo. Cada cartão tem o nome da sua configuração, para você pedir exatamente essa a um agente."
},
"sizes": {
"heading": "Tamanhos de pôster",
"waiting": "A escala de tamanhos (papel padrão, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft e 5 × 8 ft) aparecerá aqui quando a tabela de tamanhos compartilhada for publicada. Cada tamanho será mostrado como pré-visualização de impressão, apenas na vertical."
},
"family": {
"mapSheet": "Folhas de mapa",
"packet": "Pacote de oração",
"cards": "Cartões de oração",
"wall": "Painéis de parede",
"mapPrint": "Impressão do mapa"
},
"tpl": {
"mapSheetCountry": "Folha de mapa do país",
"mapSheetPacketPage": "Página de mapa do pacote",
"packetFull": "Pacote de oração, completo",
"packetLightweight": "Pacote de oração, leve",
"cards": "Cartões de oração",
"blessPanels": "Painéis do mural Abençoar",
"mapPrint": "Impressão do mapa"
},
"missing": {
"packet": "A galeria ainda não desenha o pacote. Os detalhes indicam o seu gerador e a variante.",
"cards": "A galeria ainda não desenha os cartões. Os detalhes indicam o gerador e o formato do cartão.",
"bless": "O motor deste modelo ainda não está na biblioteca compartilhada, por isso a galeria não consegue desenhá-lo.",
"mapPrint": "Este modelo imprime o mapa ao vivo que o leitor está vendo, por isso não tem imagem de exemplo."
},
"details": {
"configName": "Nome da configuração",
"copyName": "Copiar",
"size": "Tamanho",
"fileSize": "Tamanho do arquivo",
"fileSizeUnknown": "Medido quando o pôster é desenhado",
"settings": "Configurações",
"sample": "Dados de exemplo",
"renderer": "Gerador",
"legacy": "Tamanho antigo: ainda imprime, mas o seletor de impressão já não o oferece.",
"forAgents": "Para usar este modelo, diga a um agente: use a configuração {name}."
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "India — state și teritorii ale Uniunii",
"indiaStateNoGroups": "Aici nu este listat niciun popor neatins."
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "Arată toate detaliile",
"showFullDetailsAria": "Arată toate detaliile pentru {label}",
"tab": {
"styles": "Stiluri",
"sizes": "Dimensiuni"
},
"styles": {
"heading": "Stiluri de afiș",
"intro": "Toate șabloanele de afiș într-un singur loc, desenate din bibliotecă cu date de exemplu. Fiecare card poartă numele configurației sale, ca să poți cere unui agent exact acel șablon."
},
"sizes": {
"heading": "Dimensiuni de afiș",
"waiting": "Scara dimensiunilor (hârtie standard, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft și 5 × 8 ft) va apărea aici după publicarea tabelului comun de dimensiuni. Fiecare dimensiune va fi afișată ca previzualizare de tipărire, doar pe verticală."
},
"family": {
"mapSheet": "Foi de hartă",
"packet": "Pachet de rugăciune",
"cards": "Cartonașe de rugăciune",
"wall": "Panouri de perete",
"mapPrint": "Tipărirea hărții"
},
"tpl": {
"mapSheetCountry": "Foaie de hartă a țării",
"mapSheetPacketPage": "Pagina de hartă a pachetului",
"packetFull": "Pachet de rugăciune, complet",
"packetLightweight": "Pachet de rugăciune, ușor",
"cards": "Cartonașe de rugăciune",
"blessPanels": "Panourile peretelui Binecuvântează",
"mapPrint": "Tipărirea hărții"
},
"missing": {
"packet": "Galeria nu desenează încă pachetul. Detaliile numesc generatorul și varianta lui.",
"cards": "Galeria nu desenează încă cartonașele. Detaliile numesc generatorul și formatul cartonașului.",
"bless": "Motorul acestui șablon nu este încă în biblioteca comună, așa că galeria nu îl poate desena.",
"mapPrint": "Acest șablon tipărește harta live pe care o privește cititorul, așa că nu are imagine de exemplu."
},
"details": {
"configName": "Numele configurației",
"copyName": "Copiază",
"size": "Dimensiune",
"fileSize": "Dimensiunea fișierului",
"fileSizeUnknown": "Se măsoară când afișul este desenat",
"settings": "Setări",
"sample": "Date de exemplu",
"renderer": "Generator",
"legacy": "Dimensiune veche: încă se tipărește, dar selectorul de tipărire nu o mai oferă.",
"forAgents": "Ca să folosești acest șablon, spune-i unui agent: folosește configurația {name}."
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "Индия — штаты и союзные территории",
"indiaStateNoGroups": "Здесь не указано ни одного недостигнутого народа."
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "Показать все сведения",
"showFullDetailsAria": "Показать все сведения: {label}",
"tab": {
"styles": "Стили",
"sizes": "Размеры"
},
"styles": {
"heading": "Стили плакатов",
"intro": "Все шаблоны плакатов в одном месте, нарисованные из библиотеки на примерных данных. Каждая карточка названа по своей конфигурации, чтобы можно было попросить агента именно о ней."
},
"sizes": {
"heading": "Размеры плакатов",
"waiting": "Шкала размеров (стандартная бумага, 3 × 3 фута, 3 × 5 футов, 5,5 × 5,5 фута и 5 × 8 футов) появится здесь, когда будет опубликована общая таблица размеров. Каждый размер будет показан как предпросмотр печати, только в книжной ориентации."
},
"family": {
"mapSheet": "Листы карт",
"packet": "Молитвенный пакет",
"cards": "Молитвенные карточки",
"wall": "Настенные панели",
"mapPrint": "Печать карты"
},
"tpl": {
"mapSheetCountry": "Лист карты страны",
"mapSheetPacketPage": "Страница карты в пакете",
"packetFull": "Молитвенный пакет, полный",
"packetLightweight": "Молитвенный пакет, облегчённый",
"cards": "Молитвенные карточки",
"blessPanels": "Панели стены «Благословить»",
"mapPrint": "Печать карты"
},
"missing": {
"packet": "Галерея пока не рисует пакет. В сведениях указаны его генератор и вариант.",
"cards": "Галерея пока не рисует карточки. В сведениях указаны генератор и формат карточки.",
"bless": "Движка этого шаблона пока нет в общей библиотеке, поэтому галерея не может его нарисовать.",
"mapPrint": "Этот шаблон печатает живую карту, на которую смотрит читатель, поэтому у него нет примерного изображения."
},
"details": {
"configName": "Название конфигурации",
"copyName": "Копировать",
"size": "Размер",
"fileSize": "Размер файла",
"fileSizeUnknown": "Измеряется, когда плакат нарисован",
"settings": "Параметры",
"sample": "Примерные данные",
"renderer": "Генератор",
"legacy": "Устаревший размер: он ещё печатается, но в выборе печати его больше нет.",
"forAgents": "Чтобы использовать этот шаблон, скажите агенту: используй конфигурацию {name}."
}
}

View file

@ -0,0 +1,4 @@
{
"indiaStatesHeading": "印度——邦与中央直辖区",
"indiaStateNoGroups": "此处未列出任何未得之民族群。"
}

View file

@ -0,0 +1,50 @@
{
"showFullDetails": "显示完整详情",
"showFullDetailsAria": "显示{label}的完整详情",
"tab": {
"styles": "样式",
"sizes": "尺寸"
},
"styles": {
"heading": "海报样式",
"intro": "所有海报模板集中在一处,均由组件库用示例数据绘制。每张卡片都以其配置命名,方便你请代理准确使用那一种。"
},
"sizes": {
"heading": "海报尺寸",
"waiting": "共享尺寸表发布后,尺寸阶梯(标准纸张、3 × 3 英尺、3 × 5 英尺、5.5 × 5.5 英尺和 5 × 8 英尺)将显示在这里。每种尺寸都会以打印预览呈现,仅限纵向。"
},
"family": {
"mapSheet": "地图页",
"packet": "祷告资料包",
"cards": "祷告卡",
"wall": "墙面展板",
"mapPrint": "地图打印"
},
"tpl": {
"mapSheetCountry": "国家地图页",
"mapSheetPacketPage": "资料包地图页",
"packetFull": "祷告资料包(完整版)",
"packetLightweight": "祷告资料包(精简版)",
"cards": "祷告卡",
"blessPanels": "祝福墙展板",
"mapPrint": "地图打印"
},
"missing": {
"packet": "图库暂不绘制资料包。详情中列出了它的生成器和版本。",
"cards": "图库暂不绘制祷告卡。详情中列出了生成器和卡片规格。",
"bless": "此模板的引擎尚未进入共享组件库,因此图库无法绘制。",
"mapPrint": "此模板打印读者正在查看的实时地图,因此没有示例图片。"
},
"details": {
"configName": "配置名称",
"copyName": "复制",
"size": "尺寸",
"fileSize": "文件大小",
"fileSizeUnknown": "海报绘制后测量",
"settings": "设置",
"sample": "示例数据",
"renderer": "生成器",
"legacy": "旧尺寸:仍可打印,但打印选择器已不再提供。",
"forAgents": "要使用此模板,请告诉代理:使用配置 {name}。"
}
}

View file

@ -58,8 +58,9 @@
* keeps the renderer's own default rather than a fabricated position.
*
* A null sheet is a tier too: India (sheet null, too many groups for any paper size) is its own
* 1-of-1 series. Skipping it printed "map 1 of 0" on India's packet page 1 from the Regions tab
* while the wall's own copy printed "map 1 of 1" -- the two paths disagreeing again.
* 1-of-1 series. Skipping it gave India's sheet "map 1 of 0" on the Regions path and "map 1 of 1" on the
* wall's old copy. Neither was visible then (the map-only packet page prints no series line, and India's
* poster is gated), but any India sheet with a header would have shown it.
* Every null-sheet country shares that ONE oversized series: a second would make India "map 1 of 2".
* check:country-sheet-one-path fails when that happens, so it is decided, not drifted into.
*/

View file

@ -143,5 +143,49 @@
"emit-countries.mjs), so the SHAPE was replaced: not 'split the set' but 'ask which one,",
"then give them that poster'. The numbers are gone from the headings for the same reason."
]
},
"indiaState": {
"label": "India states and union territories — one poster for each of the 34 with people groups listed",
"dataDir": "plugin-designs/my-upg-100-list/data",
"countryMode": true,
"funnel": false,
"poster": {
"kicker": "Unreached People Groups",
"titleQualifier": "Pray for",
"seriesName": "India State Maps",
"sideBoxHeading": "What is a State Prayer Map?",
"sideBoxBody": "This map is one of a {approxLists}-map set called the {seriesName} — one sheet for each state and union territory of India, naming the unreached people groups who live there so a reader can pray for them by name."
},
"_poster_why": [
"A state poster is a country poster one level down: the Country Posters wall prints one for each of India's",
"states and union territories that has people groups listed (34 of 36; data/people-groups/india-states.json).",
"The country series' text cannot be reused: it would call Gujarāt's poster one of the ~170 'Country Maps',",
"one sheet for each country. The term is 'state and union territory', never 'province'. Approved as the",
"default wording 2026-09-30 and shown as editable; a change is a strings-only follow-up."
],
"strings": {
"map_page_kicker": "PRAY FOR THIS STATE",
"map_page_sub": "A prayer list of {count} unreached people groups who live here",
"points_intro": "One hour with God, five minutes each. Pray these twelve for the peoples of this state.",
"christian_lede": "This is the same grid — one human life in weeks. Now each box is a share of the people in this state. The shaded boxes are the evangelical share of their population; the rest are waiting."
},
"_strings_why": [
"The country series' four keys with 'country' read as 'state'. Used by the Node generator; the browser",
"packet reads the localized packet-strings.json, which is series-invariant."
],
"networks": [
[
"1 · Disciples.",
"Ask a disciple which state God has put on their heart, then give them that state's poster to print and prepare."
],
[
"2 · Churches.",
"List the churches you know and ask each the same question, so every church has a poster to pray over together."
],
[
"3 · Indigenous leaders.",
"Ask which leaders you know who live in one of these states, and give them their own state's map to pray for it specifically."
]
]
}
}

View file

@ -251,6 +251,7 @@
"check:window-globals-released": "node internal/check-window-globals-released.mjs",
"check:poster-card-fits": "node internal/check-poster-card-fits.mjs",
"check:poster-degrades": "node internal/check-poster-degrades-honestly.mjs",
"check:poster-design-catalogue": "node plugin-designs/poster-design/scripts/check-catalogue.mjs",
"check:poster-fetch-guard": "node internal/check-poster-fetch-guard.mjs",
"check:poster-print-fallback": "node internal/check-poster-print-fallback.mjs",
"check:posters-exportable": "node internal/check-posters-exportable.mjs",
@ -347,7 +348,7 @@
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs",
@ -451,7 +452,7 @@
"check:composite-token-url": "a composite embedded BARE (no outer ProfileLoader) must honour a tokenUrl-only config: it reads tokenUrl, forwards it, and asks the token service only when no token is given. The expectation is derived from ProfileLoader, not restated. Headless; in gates:pre.",
"check:composites-mount": "every capability-4/5 composite's OWN setup() and mounted hooks run without throwing, in en and ar, headless (no browser): vite loads each composite as its bundle entry defines it (configureApp, and ProfileLoader's provides where the entry mounts ProfileLoader), and @vue/runtime-core mounts it on a no-op renderer. Exists because 08f89f55 shipped two composites calling getRuntimeLocale/detectLocale/RTL_LOCALES with no import: the build was green, setup threw a ReferenceError in every locale, and Big Picture and Simple Research rendered nothing. Proves setup runs; cannot prove the children render.",
"check:country-names": "every ISO 3166-1 alpha-3 our data uses must resolve to a localized country name. Guards the JOIN, not Intl: library/data/iso3-to-alpha2.json drifting from library/data/countries.json makes a country render as a bare code, which still looks like a value. Checks en/ar/zh (ar is our only RTL locale; zh makes an untranslated Latin code unmistakable) and asserts BOTH directions of drift. Carries two negative arms — an unassigned code and a malformed one — and FAILS if either resolves, because a coverage count cannot otherwise tell a hit from a lookup that returns its input.",
"check:country-sheet-one-path": "a country sheet is drawn through ONE path, @map/packet/countrySheet.js, so one country cannot print two posters. Parses (comments cannot count) every plugin source naming generateCountrySheet/makeCountrySheet: a direct generateCountrySheet in code FAILS, every makeCountrySheet call must pass `countries` (the series list), and both known callers (the Country Posters wall, regions-list) must be found. Also runs seriesFor over the real country-posters.json with named answers: India 1 of 1, every 3x3 country N of the 3x3 tier with no repeat, a code absent from the data no series.",
"check:country-sheet-one-path": "a country sheet, and an Indian state or union territory sheet, each draw through ONE path (@map/packet/countrySheet.js; country-posters/src/indiaStateSheet.js), so one place cannot print two posters. Parses (comments cannot count) plugin sources: a direct generateCountrySheet FAILS anywhere, a direct generateSheet FAILS inside country-posters outside the state helper, every makeCountrySheet call must pass `countries`, and the known callers must be found. Named data answers: seriesFor over country-posters.json (India 1 of 1, each 3x3 country N of 173, an absent code no series, exactly one no-sheet country); over the served india-states.json + gb-IND-ADM1.geojson: 36 divisions, the 34 with groups numbered 1..34 each with an outline, the rest named, the shared ADM1 never mutated.",
"check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.",
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
@ -579,11 +580,12 @@
"check:print-frame-reclaim": "a print frame is kept until it has printed: openPrintFrame reclaims it 60 s after its load (not after its creation), at afterprint, or at a 10-minute ceiling, and printImageBlob revokes its object URL on the same clock. Fake timers; two-sided (the pre-fix copy fails). The Wall poster loads 132 MB of photos, and the old 60 s-from-creation clock removed the frame before it could print.",
"check:poster-card-fits": "a poster card's print-options row must stay inside its own card. Reported by the repo agent off a 1440px screenshot: 27/27 cards overflowing by 110px, 36 neighbour intrusions. .pfs already had width:82%, so SOURCE READING SAID IT WAS FINE — flex items default to min-width:auto and refuse to shrink. Asserts BOTH no-overlap AND that the controls keep a usable box, because a row whose buttons collapsed to nothing would also \"not overflow\". Sweeps 390/768/1024/1440/1920px and asserts the row sits BELOW the preview too — measuring only left/right edges is how it passed a driver-visible regression. CARD_FIT_WIDTH overrides the list. SKIPs when nothing is served.",
"check:poster-degrades": "with NO stored poster, NO source and NO generator (forced in the browser: every posters/*.webp and *.html answers 404, lib/poster-render.js is aborted), nothing must print and the reader must be told why. Measures any served tree; NOT MEASURED without a wall url.",
"check:poster-design-catalogue": "Poster Design's configuration catalogue (plugin-designs/poster-design/src/templates.js) says only true things about the library: its sheet formats are exactly poster-render.js FORMATS at the same pixel sizes (the generator is a pinned asset, so the table cannot be imported); every configuration name is unique and well formed; the card entries are exactly card-render STOCK; every catalogue key it names exists in en/posterDesign.json; the sample country can be drawn. Six mutants (px drift, a missing format, a duplicate name, a bad key, an undrawable sample, a dropped stock) each FAIL.",
"check:poster-fetch-guard": "a host's 200 fallback page is never taken for a poster. On a drop-in dev host a missing posters/<id>.webp answered 200 text/html, and export handed that page to the print dialog as the image. Asserts library/packet/poster-fetch.js accepts a raster only if it is an image (by type or by its first bytes) and a source only if it is a poster document (class=\"stage\"), and that neither usePosterPrints nor PosterWall fetches posters/ directly. Node only, no server.",
"check:poster-layout-optin": "the composed poster sheet (header, map, legend beside it, footer) is OPT-IN: spec.layout === 'poster' asks for it, and every caller that does not gets the full-page map exactly as before, asserted by object identity, not just by value. Drives the real layout decisions with real spec objects. Headless, no browser.",
"check:poster-print-fallback": "clicks the REAL poster print control with the served poster generator and every posters/*.webp aborted, and asserts the preview RASTERISES from posters/<id>.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; NOT MEASURED otherwise.",
"check:posters-exportable": "every poster the tree offers can be printed. With posterSources on, the tree once shipped only the 28 zone sources, so 350 of 378 posters said \"poster not found (404)\". Each id, from data/posters and the wall's previews, must ship a real WebP (RIFF..WEBP) or a real poster source (its class=\"stage\" element), so a placeholder or a host's fallback page cannot pass. SKIPs when posters are off by config, which check:poster-degrades covers. Reads the built tree; no server.",
"check:posterverse-shelf": "Posterverse (the capability-5 poster shell) shows exactly its allow-list and never a gated plugin. Asserts the rail rows per section (Posters: upg-100, country-posters, poster-design; Experimental: wall-of-people-groups, all-people-groups, upg-neighbor, bible-posters), that no row names key-cities or a shell (a key-cities bundle is planted into the built manifest, because the real one withholds gated profiles), that every present row opens the profile it names (no silent fallback to a leaf), that posterverse.meta.json composes equals the rows whose bundle exists, and 11-locale parity of the shell strings. --self-test plants one defect per arm. Headless. Exit 0 pass, 1 fail, 75 no built manifest.",
"check:posterverse-shelf": "Posterverse (the capability-5 poster shell) shows exactly its allow-list and never a gated plugin. Asserts the rail rows per section (Posters: upg-100, country-posters, poster-design; Experimental: wall-of-people-groups, bible-posters), that no row names key-cities or a shell (a key-cities bundle is planted into the built manifest, because the real one withholds gated profiles), that every present row opens the profile it names (no silent fallback to a leaf), that posterverse.meta.json composes equals the rows whose bundle exists, and 11-locale parity of the shell strings. --self-test plants one defect per arm. Headless. Exit 0 pass, 1 fail, 75 no built manifest.",
"check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT=<dir> so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.",
"check:preset-config-merge": "a preset page (<bundle>/<profile>/<instance>/index.html) keeps every default in its profile's .meta.json config, with the instance's props merged over them (go-int's upg-neighbor presets lost pinHint:false when props replaced the config). Reads the built tree; SKIP without one.",
"check:profile-table": "the prayer packet's profile TABLE (20 full-width rows: picture, generated description, three boxes; PER_PAGE stays 20): a row stays tall enough for a name plus a two-line description at the smallest fit size, and truncation happens only at a whole word. Character-count, not DOM, so Node and the browser agree. Cannot see the RENDERED row height: that is check:packet-render, a browser gate. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",

View file

@ -35,7 +35,7 @@
* The shared wall always sorts sections AND cards by descending count, so the
* dense countries lead and the long tail sinks. There is no floor.
*/
import { computed, inject, ref } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import PosterWall from '@map/components/poster/PosterWall.vue'
import { useOptionalT, useOptionalI18n } from '@map/composables/useOptionalT.js'
// A country's name in the reader's language (Intl, joined from our ISO-3 codes).
@ -49,6 +49,9 @@ import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
import data from '@map/data/country-posters.json'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
import { makeCountrySheet } from '@map/packet/countrySheet.js'
// India's states and union territories: the shared table (fetched when the wall shows them) and their sheet.
import { loadIndiaStates } from '@map/data/indiaStates.js'
import { makeIndiaStateSheet, statesWithPosters } from './src/indiaStateSheet.js'
// The regions catalogue holds the not-yet sentence a host's country gets when no poster exists (all eleven locales);
// its keys are English sentences with dots, so it is read by literal key, the way regions-list.vue reads it.
import { useRegionsI18n } from '@map/i18n/useRegionsI18n.js'
@ -105,15 +108,36 @@ const hostColumns = computed(() => Number(profileConfig.value?.columns) || 2)
let tRegions = (key) => key
try { tRegions = useRegionsI18n().t } catch (_) { /* a host with no i18n reads the English sentence, like every string here */ }
// ★ INDIA'S STATES AND UNION TERRITORIES, one poster each, like a country one level down. The table is
// fetched (and sha-checked) only when the unfiltered wall is shown; a host's filtered wall never asks for it.
// A failure hides the section rather than showing cards that cannot print. States with no people groups
// listed (Nagaland, Mizoram) are NAMED with a card that says so, never silently left out.
const indiaStates = ref(null)
watch(filtered, (f) => {
if (f || indiaStates.value) return
loadIndiaStates(assetsBase.value).then((table) => { indiaStates.value = table.states }).catch(() => { indiaStates.value = null })
}, { immediate: true })
const INDIA_REGION = (byIso3.get('IND') || {}).region || 'Asia'
const stateItems = computed(() => {
const all = indiaStates.value
if (filtered.value || !all) return []
const withGroups = statesWithPosters(all).map((st) => ({
id: st.id, label: st.name, count: st.count, sheet: '3x3', region: INDIA_REGION, indiaState: true, slugs: st.slugs,
}))
const none = all.filter((st) => !st.count).map((st) => ({ id: st.id, label: st.name, count: 0, indiaState: true, noGroups: true, slugs: [] }))
return [...withGroups, ...none]
})
const items = computed(() => {
const l = wallLocale && (wallLocale.value ?? wallLocale)
const locale = (typeof l === 'string' && l) || getRuntimeLocale() || detectLocale() || 'en'
const countries = selected.value
if (/^en\b/i.test(locale)) return countries
return countries.map((c) => {
// State names stay as the table has them (place names from the data); only country names are localized.
if (/^en\b/i.test(locale)) return [...countries, ...stateItems.value]
return [...countries.map((c) => {
const name = countryName(c.iso3, locale)
return name && name !== c.iso3 ? { ...c, label: name } : c
})
}), ...stateItems.value]
})
// ★ EVERY COUNTRY PRINTS ITS PRAYER RESOURCES — the list, the card deck and the packet — through the
@ -125,7 +149,9 @@ const recordsFor = createPeopleGroupRecords({
lang: () => getRuntimeLocale() || detectLocale(),
profileConfig,
})
const countryRecords = (c) => recordsFor({ type: 'country', values: [c.iso3] })
const countryRecords = (c) => (c.indiaState
? (c.slugs.length ? recordsFor({ type: 'pg_slugs', values: c.slugs }) : Promise.resolve([]))
: recordsFor({ type: 'country', values: [c.iso3] }))
// ★ GENERATED AT THE CLICK, through the ONE shared path (@map/packet/countrySheet.js, which the Regions
// tab uses too): the country's sheet is drawn in the browser from its people groups and the world
// boundaries, for the poster print (letter, vector) and the packet's page 1. No stored sheet exists for a
@ -139,7 +165,12 @@ const sheetForRecord = makeCountrySheet({
assetsBase: () => assetsBase.value,
countries: data.countries || [],
})
const countrySheet = (card, format) => sheetForRecord(byId.get(card.id) || card, format)
const countryOnlySheet = (card, format) => sheetForRecord(byId.get(card.id) || card, format)
// A state's sheet through its own ONE path (src/indiaStateSheet.js), in India's hue and the indiaState series.
const stateSheet = makeIndiaStateSheet({
records: recordsFor, assetsBase: () => assetsBase.value, states: () => indiaStates.value, region: INDIA_REGION,
})
const countrySheet = (card, format) => (card.indiaState ? stateSheet(card, format) : countryOnlySheet(card, format))
// The name table the generated sheet was labelled from, so a packet in another language relabels page 1's
// country names (the same seam the UPG-100 wall passes).
const iso3Names = () => import('@map/data/iso3-name.js').then((m) => m.ISO3_NAME)
@ -161,6 +192,7 @@ async function countryMap(c) {
const t = useOptionalT('')
const regionSlug = (label) => String(label).toLowerCase().replace(/\s+/g, '_')
const groupBy = (c) => {
if (c.indiaState) return t('countryPosters.indiaStatesHeading')
if (!c.region || c.region === 'Other') return t('dashboard.other')
const key = `dashboard.wagfRegion.${regionSlug(c.region)}`
const v = t(key)
@ -178,7 +210,7 @@ const collapsedBy = (sec) => sec.total < 60
// unsupportedReason is an English note (and a flag), not display text. The card already states the
// count above it, so the sentence needs no number and no plural agreement.
// A requested country with no poster at all says so in regions-list.vue's words (the regions catalogue).
const unsupported = (c) => (c.noPoster ? tRegions('No poster has been generated for this country yet.') : c.unsupportedReason ? t('messages.countryPosterTooMany') : null)
const unsupported = (c) => (c.noGroups ? t('countryPosters.indiaStateNoGroups') : c.noPoster ? tRegions('No poster has been generated for this country yet.') : c.unsupportedReason ? t('messages.countryPosterTooMany') : null)
// ★ THROUGH THE CATALOGUE, like the UPG-100 wall: the heading, the tag, the intro and the counts read
// in the page's language. The intro keeps its one emphasis as three plain keys (A, the emphasis, B)

View file

@ -183,7 +183,10 @@ function onShowOnMap(e) {
// The attribute carries the country ID, not its geometry: countries-map resolves the id against
// the same library/data/country-posters.json the wall renders from, so the bbox has exactly one
// source of truth rather than being copied through an attribute.
const countryId = d.listId ?? null
// An Indian state's card (the state table's ids are 'india-<code>') shows India: this map draws countries,
// not states, so framing the country is the honest answer rather than a world view with nothing selected.
const listId = d.listId ?? null
const countryId = typeof listId === 'string' && listId.startsWith('india-') ? 'country-ind' : listId
const el = paneEls.map
if (!el) return
if (countryId) el.setAttribute('select-country', String(countryId))

View file

@ -25,6 +25,10 @@ import { parse as parseSfc, babelParse } from '@vue/compiler-sfc'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
const PD = join(ROOT, 'plugin-designs')
const KNOWN_CALLERS = ['plugin-designs/country-posters/country-poster-wall.vue', 'plugin-designs/regions-dashboard/regions-list.vue']
// India's states and union territories draw through their own ONE path; inside this plugin only that helper may
// call the generator directly.
const STATE_HELPER = 'plugin-designs/country-posters/src/indiaStateSheet.js'
const STATE_CALLER = 'plugin-designs/country-posters/country-poster-wall.vue'
function scriptsOf(file, src) {
if (!file.endsWith('.vue')) return [src]
@ -78,9 +82,11 @@ function listSources(dir, out = []) {
export function armA(files, callers = []) {
const fails = []
for (const [rel, src] of Object.entries(files)) {
if (!/generateCountrySheet|makeCountrySheet/.test(src)) continue // a text prefilter; the parse decides
if (!/generateCountrySheet|makeCountrySheet|generateSheet|makeIndiaStateSheet/.test(src)) continue // a text prefilter; the parse decides
const { names, makeCalls } = codeFacts(rel, src)
if (names.has('generateCountrySheet')) fails.push(`${rel}: draws a country sheet directly (generateCountrySheet in code); use makeCountrySheet from @map/packet/countrySheet.js`)
if (rel.startsWith('plugin-designs/country-posters/') && rel !== STATE_HELPER && names.has('generateSheet')) fails.push(`${rel}: calls generateSheet directly; a state sheet goes through makeIndiaStateSheet (${STATE_HELPER})`)
if (rel === STATE_CALLER && names.has('makeIndiaStateSheet')) callers.push(`${rel}#state`)
if (makeCalls.length) callers.push(rel)
makeCalls.forEach((c, i) => { if (!c.hasCountries) fails.push(`${rel}: makeCountrySheet call #${i + 1} passes no \`countries\`, so its "map N of M" is not the wall's`) })
}
@ -111,6 +117,23 @@ export function armB(seriesFor, countries) {
return fails
}
// ARM C: the state posters over the SERVED table and the renderer's own ADM1 file (named expectations).
export function armC(H, states, adm1Ind) {
const fails = []
const before = JSON.stringify((adm1Ind.features || []).map((f) => f.properties && f.properties.listId)) // BEFORE any call
const withGroups = H.statesWithPosters(states)
const zero = (states || []).filter((st) => !st.count).map((st) => st.name)
if ((states || []).length !== 36) fails.push(`the state table has ${(states || []).length} divisions, expected 36 (28 states + 8 union territories)`)
if (withGroups.length + zero.length !== (states || []).length) fails.push('every division is either postered or named as having none')
const idx = withGroups.map((st) => (H.stateSeries(states, st) || {}).index)
if (new Set(idx).size !== withGroups.length || Math.min(...idx) !== 1 || Math.max(...idx) !== withGroups.length) fails.push(`state series indexes are not 1..${withGroups.length}, unique: ${idx.join(',')}`)
const noShape = withGroups.filter((st) => !H.stateGeometry(H.stateZone(st, '#000'), adm1Ind, st.shapeName)).map((st) => st.name)
if (noShape.length) fails.push(`states with people groups but no outline in gb-IND-ADM1.geojson (their poster would not draw): ${noShape.join(', ')}`)
withGroups.forEach((st) => H.stateGeometry(H.stateZone(st, '#000'), adm1Ind, st.shapeName))
if (JSON.stringify((adm1Ind.features || []).map((f) => f.properties && f.properties.listId)) !== before) fails.push('stateGeometry mutated the shared ADM1 collection')
return fails
}
async function main() {
const selfTest = process.argv.includes('--self-test')
const { seriesFor } = await import(join(ROOT, 'library/packet/countrySheet.js'))
@ -118,6 +141,11 @@ async function main() {
const countries = data.countries || []
if (countries.length < 170) { console.log(`NOT MEASURED: country-posters.json parsed ${countries.length} countries (floor 170)`); process.exitCode = 75; return }
const H = await import(join(ROOT, STATE_HELPER))
const states = JSON.parse(readFileSync(join(ROOT, 'public/dx-maps/assets/people-groups/india-states.json'), 'utf8')).states || []
const adm1Ind = JSON.parse(readFileSync(join(ROOT, 'public/dx-maps/assets/boundaries/gb-IND-ADM1.geojson'), 'utf8'))
if (states.length < 30 || (adm1Ind.features || []).length < 30) { console.log(`NOT MEASURED: state table ${states.length} / ADM1 ${(adm1Ind.features || []).length} features (floor 30)`); process.exitCode = 75; return }
const files = {}
for (const p of listSources(PD)) files[relative(ROOT, p)] = readFileSync(p, 'utf8')
@ -129,6 +157,10 @@ async function main() {
['India skipped (the old guard)', () => armB((l, it) => (!it || !it.sheet ? undefined : seriesFor(l, it)), countries)],
['absent code given a position', () => armB((l, it) => seriesFor(l, it) || { index: 1, totalLists: 1, totalGroups: 0 }, countries)],
['a second no-sheet country joins India', () => armB(seriesFor, countries.map((c) => (c.iso3 === 'CHN' ? { ...c, sheet: null } : c)))],
['the wall calls generateSheet directly', () => armA({ ...files, [wall]: files[wall].replace('<script setup>', "<script setup>\nimport { generateSheet } from '@map/packet/posterSheets.js'\ngenerateSheet({})") })],
['a state with groups loses its outline', () => armC(H, states.map((st) => (st.id === 'india-gj' ? { ...st, shapeName: 'Nowhere' } : st)), adm1Ind)],
['a division silently dropped', () => armC(H, states.filter((st) => st.id !== 'india-mz'), adm1Ind)],
['a helper that mutates the shared ADM1', () => armC({ ...H, stateGeometry: (z, a, n) => { (a.features || []).forEach((f) => { if (f.properties.shapeName === n) f.properties.listId = z.id }); return H.stateGeometry(z, a, n) } }, states, JSON.parse(JSON.stringify(adm1Ind)))],
]
let bad = 0
for (const [name, run] of cases) {
@ -145,12 +177,12 @@ async function main() {
}
const callers = []
const fails = [...armA(files, callers), ...armB(seriesFor, countries)]
const missing = KNOWN_CALLERS.filter((k) => !callers.includes(k))
const fails = [...armA(files, callers), ...armB(seriesFor, countries), ...armC(H, states, adm1Ind)]
const missing = [...KNOWN_CALLERS, `${STATE_CALLER}#state`].filter((k) => !callers.includes(k))
if (missing.length) fails.push(`known callers not found calling makeCountrySheet: ${missing.join(', ')}`)
if (fails.length) { fails.forEach((f) => console.log(`FAIL ${f}`)); process.exitCode = 1; return }
const ind = seriesFor(countries, countries.find((c) => c.iso3 === 'IND'))
console.log(`PASS check:country-sheet-one-path — ${Object.keys(files).length} plugin sources parsed-or-skipped, makeCountrySheet callers: ${callers.join(', ')}; seriesFor over ${countries.length} countries (India ${ind.index} of ${ind.totalLists})`)
console.log(`PASS check:country-sheet-one-path — ${Object.keys(files).length} plugin sources parsed-or-skipped, makeCountrySheet callers: ${callers.join(', ')}; seriesFor over ${countries.length} countries (India ${ind.index} of ${ind.totalLists}); ${H.statesWithPosters(states).length} state posters, series 1..${H.statesWithPosters(states).length}, every outline found`)
}
main().catch((e) => { console.error(`FAIL ${e.stack || e}`); process.exitCode = 1 })

View file

@ -0,0 +1,86 @@
/**
* indiaStateSheet.js — one Indian state's or union territory's map sheet, GENERATED in the browser at the click,
* the same print standard as a country's (@map/packet/countrySheet.js): no sheet is stored, only previews.
*
* A state poster is a country poster one level down. Its people groups come from the shared state table
* (library/data/indiaStates.js: each state's slugs), its outline from the ADM1 boundaries the renderer
* already loads (boundaries/gb-IND-ADM1.geojson, matched on shapeName), and its text from the `indiaState`
* series (series.json), so it never calls itself one of the Country Maps.
*
* const sheet = makeIndiaStateSheet({ records, assetsBase: () => base, states })
* <PosterWall :sheet="sheet" /> // (item, format) -> html, '' when it cannot be drawn
*
* ONE PATH: the wall's state cards draw through this and nothing else (check:country-sheet-one-path).
*/
// The states that get a poster: those with people groups listed, in the wall's own order (most groups first,
// then by name), so a sheet's "map N of M" matches the card's place on the wall.
export function statesWithPosters(states) {
return (states || []).filter((s) => s && s.count > 0)
.slice().sort((a, b) => b.count - a.count || String(a.name).localeCompare(String(b.name), 'en'))
}
export function stateSeries(states, state) {
const tier = statesWithPosters(states)
const index = tier.findIndex((s) => s.id === state?.id) + 1
if (!index) return undefined
return { totalGroups: tier.reduce((a, s) => a + s.count, 0), totalLists: tier.length, index }
}
// The zone the renderer draws. `provinceLabel` makes the title the state's own name, and the header's
// "Administrative Areas" row lists it (the renderer never says "province" for India).
export function stateZone(state, color) {
return {
id: state.id, zoneKey: state.id, label: state.name, provinceLabel: state.name, provinces: [state.name],
count: state.count, color,
}
}
// The renderer knows a zone's OWN ADM1 shapes by `listId === zone.id`; the served file carries UPG-100's zone
// ids there. So it gets a COPY with this state's feature re-tagged (the cached collection is shared, never
// mutated), plus the stamped zone geometry. null when the state's outline is not in the file.
export function stateGeometry(zone, adm1Ind, shapeName) {
const feats = (adm1Ind && adm1Ind.features) || []
const own = feats.filter((f) => f.properties && f.properties.shapeName === shapeName)
if (!own.length) return null
const adm1 = { ...adm1Ind, features: feats.map((f) => (own.includes(f) ? { ...f, properties: { ...f.properties, listId: zone.id } } : f)) }
const zoneFC = {
type: 'FeatureCollection',
features: own.map((f) => ({
type: 'Feature',
properties: { ...f.properties, listId: zone.id, zoneKey: zone.zoneKey, zoneColor: zone.color, zoneLabel: zone.label },
geometry: f.geometry,
})),
}
return { adm1, zoneFC }
}
/**
* @param {object} o
* records (selector) => Promise<rows[]> the shared provider; asked for the state's slugs
* assetsBase () => string where boundaries/ is served from
* states () => states[] | null the state table's entries (library/data/indiaStates.js)
* region the WAGF region the hue comes from (India's: 'Asia'), so a state prints in India's hue
* @returns {(item, format) => Promise<string>}
*/
export function makeIndiaStateSheet({ records, assetsBase, states, region = 'Asia' }) {
return async function indiaStateSheet(item, format) {
const all = states() || []
const state = all.find((s) => s.id === (item && item.id))
if (!state || !state.count) return ''
const [{ generateSheet, loadBoundaries, pinsFrom, countryRegionColor }, { ISO3_NAME }, rows] = await Promise.all([
import('@map/packet/posterSheets.js'), import('@map/data/iso3-name.js'),
records({ type: 'pg_slugs', values: state.slugs }),
])
if (!rows || !rows.length) return ''
const bounds = await loadBoundaries(assetsBase())
const zone = stateZone(state, countryRegionColor(region))
const geo = stateGeometry(zone, bounds.adm1Ind, state.shapeName)
if (!geo) return '' // no outline to draw: fall through to the stored preview, never a guessed map
return generateSheet({
zone, pins: pinsFrom(rows), zoneFC: geo.zoneFC, format, seriesId: 'indiaState', ISO3_NAME,
adm0: bounds.adm0, adm1Ind: geo.adm1, adm1Chn: bounds.adm1Chn,
series: stateSeries(all, state), assetsBase: assetsBase(),
})
}
}

View file

@ -65,7 +65,7 @@ Add `?tab=<id>` to the `src` to open a specific map.
| `home-label` | "Back to homepage" in the gallery’s language | The link’s text, in your own words. |
Tab ids: `dashboard-plus-grid` (Big Picture), `research-map-plus-grid` (Simple Research),
`regions-dashboard`, `upg-100` (UPG 100 List Posters), `prayer-map` (Prayer Progress), `country-posters`, `cities-internet`,
`regions-dashboard`, `upg-100` (UPG 100 List Posters), `prayer-map` (Prayer Progress), `country-posters`, `poster-design`, `cities-internet`,
`gospel-resources`, and every Experimental bundle by its bundle name.
## Events

View file

@ -1,5 +1,5 @@
{
"_comment": "Declares the composability tier. 'nested-mfe-spa' (display alias 'app-shell') = Capability-5: this plugin is the OUTER app shell of the dx-maps app, the Research Gallery. Its single element <doxa-maps-spa> renders a vertical-tab rail whose tabs each mount a REAL, independently embeddable plugin: the shell loads that bundle's script (URL from manifest.json) and creates its registered custom element in the pane. There is no iframe mode. Tab order and grouping live in NAV_MODEL/TAB_DEFS in doxa-maps-spa.vue; `composes` below lists the eight curated tabs as bundle/profile and must be kept in step with TAB_DEFS (check:composite reads it; the studio's \"Inside doxa-maps-spa\" tree shows it). Bundles not placed in a section appear under Experimental automatically. The tier is now DERIVED from the `composes` edges below at tree generation (a profile that composes anything is cap-4; one that composes a cap-4 is cap-5), and this explicit `type` is CHECKED against that derivation rather than being the only source: if the two disagree the build fails, because one of them is stale. discover.mjs still never infers tiers 3-5 on its own.",
"_comment": "Declares the composability tier. 'nested-mfe-spa' (display alias 'app-shell') = Capability-5: this plugin is the OUTER app shell of the dx-maps app, the Research Gallery. Its single element <doxa-maps-spa> renders a vertical-tab rail whose tabs each mount a REAL, independently embeddable plugin: the shell loads that bundle's script (URL from manifest.json) and creates its registered custom element in the pane. There is no iframe mode. Tab order and grouping live in NAV_MODEL/TAB_DEFS in doxa-maps-spa.vue; `composes` below lists the nine curated tabs as bundle/profile and must be kept in step with TAB_DEFS (check:composite reads it; the studio's \"Inside doxa-maps-spa\" tree shows it). Bundles not placed in a section appear under Experimental automatically. The tier is now DERIVED from the `composes` edges below at tree generation (a profile that composes anything is cap-4; one that composes a cap-4 is cap-5), and this explicit `type` is CHECKED against that derivation rather than being the only source: if the two disagree the build fails, because one of them is stale. discover.mjs still never infers tiers 3-5 on its own.",
"type": "nested-mfe-spa",
"composes": [
"dashboard-plus-grid/dashboard-plus-grid",
@ -7,9 +7,10 @@
"regions-dashboard/regions-mfe",
"my-upg-100-list/upg-100-mfe",
"country-posters/country-posters-mfe",
"poster-design/poster-design-mfe",
"doxa-simple-map/doxa-simple-map",
"cities-internet/connectivity-map",
"gospel-resources/resources-map"
],
"label": "Deep Research (vertical-tab app shell: the eight production maps)"
"label": "Deep Research (vertical-tab app shell: the nine production maps)"
}

View file

@ -281,14 +281,15 @@ const NAV_MODEL = [
//
// THE POSTER ROWS (2026-09-30): the section now runs Prayer Progress, UPG 100 List Posters, Country Posters,
// then Poster Design. The same poster plugins also live in the Posterverse shell; each is ONE bundle, mounted
// by whichever shell opens it. Poster Design joins this list when its bundle exists: a row whose bundle is
// not in the manifest is dropped without a word (resolveTabsFromManifest), so listing it early shows nothing.
// by whichever shell opens it, under the same tab ids in both. A row whose bundle is not in the manifest is
// dropped without a word (resolveTabsFromManifest), so a row lands only after its bundle does.
{ key: 'prayer-mobilization', label: 'Prayer Mobilization', i18n: 'sec.prayer', icon: 'heart',
tabs: [
// Prayer Progress, then UPG 100 List Posters, then Country Posters: the published order, fixed.
// Prayer Progress, UPG 100 List Posters, Country Posters, then Poster Design: the published order, fixed.
{ id: 'prayer-map', icon: 'embed' },
{ id: 'upg-100', icon: 'list' },
{ id: 'country-posters', icon: 'poster' },
{ id: 'poster-design', icon: 'grid' },
] },
// Gospel Research — RENAMED section (was "Research"). Cities with Internet then Gospel Resources,
// per the spec's table (rows 5-6). The plain, uncomposited "Research Map" tab (doxa-research-map)
@ -497,6 +498,12 @@ const TAB_DEFS = [
id: 'country-posters', label: 'Country Posters', sub: 'a poster for every country', emoji: '🗺️',
bundle: 'country-posters', profile: 'country-posters-mfe'
},
{
// The capability-4 composite again (Styles + Sizes under one view strip); poster-styles and poster-sizes
// are its single views. It draws posters, not a map, so it holds no WebGL context and is not HEAVY.
id: 'poster-design', label: 'Poster Design', sub: 'every poster style and size', emoji: '🎨',
bundle: 'poster-design', profile: 'poster-design-mfe'
},
// ── NOTE: there are deliberately NO hardcoded Experimental entries here. Every built bundle not
// named above is discovered from manifest.json at runtime and appended as an Experimental tab
// (see discoverExperimental + NOT_EXPERIMENTAL). That is what keeps a NEW plugin visible with

View file

@ -34,7 +34,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Toggle list", openViews: "Open the list of views", expandList: "Expand list", collapseList: "Collapse list", collapse: "Collapse", home: "Back to homepage",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "Prayer Walk Planner", 'tab.upg-neighbor': "UPG Neighbor", 'tab.key-cities': "Key Cities", 'tab.all-people-groups': "All People Groups", 'tab.pplr-data-maps': "PPLR Data Maps", 'tab.wall-of-people-groups': "Wall of People Groups", 'tab.country-posters': "Country Posters", 'sub.country-posters': "a poster for every country", 'tab.doxa-vision-card': "Doxa Vision Card",
'tab.prayer-walk-planner': "Prayer Walk Planner", 'tab.upg-neighbor': "UPG Neighbor", 'tab.key-cities': "Key Cities", 'tab.all-people-groups': "All People Groups", 'tab.pplr-data-maps': "PPLR Data Maps", 'tab.wall-of-people-groups': "Wall of People Groups", 'tab.country-posters': "Country Posters", 'sub.country-posters': "a poster for every country", 'tab.poster-design': "Poster Design", 'sub.poster-design': "every poster style and size", 'tab.doxa-vision-card': "Doxa Vision Card",
},
es: {
backToTop: 'Volver arriba',
@ -55,7 +55,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Mostrar u ocultar la lista", openViews: "Abrir la lista de vistas", expandList: "Expandir la lista", collapseList: "Contraer la lista", collapse: "Contraer", home: "Volver a la página de inicio",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "Caminatas de oración", 'tab.upg-neighbor': "Vecino UPG", 'tab.key-cities': "Ciudades clave", 'tab.all-people-groups': "Todos los pueblos", 'tab.pplr-data-maps': "Mapas de datos PPLR", 'tab.wall-of-people-groups': "Muro de los pueblos", 'tab.country-posters': "Carteles por país", 'sub.country-posters': "un cartel para cada país", 'tab.doxa-vision-card': "Tarjeta de visión Doxa",
'tab.prayer-walk-planner': "Caminatas de oración", 'tab.upg-neighbor': "Vecino UPG", 'tab.key-cities': "Ciudades clave", 'tab.all-people-groups': "Todos los pueblos", 'tab.pplr-data-maps': "Mapas de datos PPLR", 'tab.wall-of-people-groups': "Muro de los pueblos", 'tab.country-posters': "Carteles por país", 'sub.country-posters': "un cartel para cada país", 'tab.poster-design': "Diseño de carteles", 'sub.poster-design': "todos los estilos y tamaños de cartel", 'tab.doxa-vision-card': "Tarjeta de visión Doxa",
},
fr: {
backToTop: 'Retour en haut',
@ -76,7 +76,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Afficher ou masquer la liste", openViews: "Ouvrir la liste des vues", expandList: "Déplier la liste", collapseList: "Replier la liste", collapse: "Replier", home: "Retour à l’accueil",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "Marches de prière", 'tab.upg-neighbor': "Voisin UPG", 'tab.key-cities': "Villes clés", 'tab.all-people-groups': "Tous les peuples", 'tab.pplr-data-maps': "Cartes de données PPLR", 'tab.wall-of-people-groups': "Mur des peuples", 'tab.country-posters': "Affiches par pays", 'sub.country-posters': "une affiche pour chaque pays", 'tab.doxa-vision-card': "Carte de vision Doxa",
'tab.prayer-walk-planner': "Marches de prière", 'tab.upg-neighbor': "Voisin UPG", 'tab.key-cities': "Villes clés", 'tab.all-people-groups': "Tous les peuples", 'tab.pplr-data-maps': "Cartes de données PPLR", 'tab.wall-of-people-groups': "Mur des peuples", 'tab.country-posters': "Affiches par pays", 'sub.country-posters': "une affiche pour chaque pays", 'tab.poster-design': "Conception d’affiches", 'sub.poster-design': "tous les styles et formats d’affiche", 'tab.doxa-vision-card': "Carte de vision Doxa",
},
de: {
backToTop: 'Nach oben',
@ -97,7 +97,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Liste ein-/ausblenden", openViews: "Ansichtsliste öffnen", expandList: "Liste ausklappen", collapseList: "Liste einklappen", collapse: "Einklappen", home: "Zurück zur Startseite",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "Gebetsspaziergänge", 'tab.upg-neighbor': "UPG-Nachbar", 'tab.key-cities': "Schlüsselstädte", 'tab.all-people-groups': "Alle Volksgruppen", 'tab.pplr-data-maps': "PPLR-Datenkarten", 'tab.wall-of-people-groups': "Wand der Volksgruppen", 'tab.country-posters': "Länderposter", 'sub.country-posters': "ein Poster für jedes Land", 'tab.doxa-vision-card': "Doxa-Visionskarte",
'tab.prayer-walk-planner': "Gebetsspaziergänge", 'tab.upg-neighbor': "UPG-Nachbar", 'tab.key-cities': "Schlüsselstädte", 'tab.all-people-groups': "Alle Volksgruppen", 'tab.pplr-data-maps': "PPLR-Datenkarten", 'tab.wall-of-people-groups': "Wand der Volksgruppen", 'tab.country-posters': "Länderposter", 'sub.country-posters': "ein Poster für jedes Land", 'tab.poster-design': "Postergestaltung", 'sub.poster-design': "alle Posterstile und -größen", 'tab.doxa-vision-card': "Doxa-Visionskarte",
},
pt: {
backToTop: 'Voltar ao topo',
@ -118,7 +118,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Mostrar ou ocultar a lista", openViews: "Abrir a lista de visualizações", expandList: "Expandir a lista", collapseList: "Recolher a lista", collapse: "Recolher", home: "Voltar à página inicial",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "Caminhadas de oração", 'tab.upg-neighbor': "Vizinho UPG", 'tab.key-cities': "Cidades-chave", 'tab.all-people-groups': "Todos os povos", 'tab.pplr-data-maps': "Mapas de dados PPLR", 'tab.wall-of-people-groups': "Mural dos povos", 'tab.country-posters': "Cartazes por país", 'sub.country-posters': "um cartaz para cada país", 'tab.doxa-vision-card': "Cartão de visão Doxa",
'tab.prayer-walk-planner': "Caminhadas de oração", 'tab.upg-neighbor': "Vizinho UPG", 'tab.key-cities': "Cidades-chave", 'tab.all-people-groups': "Todos os povos", 'tab.pplr-data-maps': "Mapas de dados PPLR", 'tab.wall-of-people-groups': "Mural dos povos", 'tab.country-posters': "Cartazes por país", 'sub.country-posters': "um cartaz para cada país", 'tab.poster-design': "Design de cartazes", 'sub.poster-design': "todos os estilos e tamanhos de cartaz", 'tab.doxa-vision-card': "Cartão de visão Doxa",
},
it: {
backToTop: 'Torna su',
@ -141,7 +141,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Mostra o nascondi l’elenco", openViews: "Apri l’elenco delle viste", expandList: "Espandi l’elenco", collapseList: "Comprimi l’elenco", collapse: "Comprimi", home: "Torna alla home page",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "Passeggiate di preghiera", 'tab.upg-neighbor': "Vicino UPG", 'tab.key-cities': "Città chiave", 'tab.all-people-groups': "Tutti i popoli", 'tab.pplr-data-maps': "Mappe dati PPLR", 'tab.wall-of-people-groups': "Muro dei popoli", 'tab.country-posters': "Poster per paese", 'sub.country-posters': "un poster per ogni paese", 'tab.doxa-vision-card': "Scheda visione Doxa",
'tab.prayer-walk-planner': "Passeggiate di preghiera", 'tab.upg-neighbor': "Vicino UPG", 'tab.key-cities': "Città chiave", 'tab.all-people-groups': "Tutti i popoli", 'tab.pplr-data-maps': "Mappe dati PPLR", 'tab.wall-of-people-groups': "Muro dei popoli", 'tab.country-posters': "Poster per paese", 'sub.country-posters': "un poster per ogni paese", 'tab.poster-design': "Design dei poster", 'sub.poster-design': "tutti gli stili e i formati dei poster", 'tab.doxa-vision-card': "Scheda visione Doxa",
},
ro: {
backToTop: 'Sus',
@ -162,7 +162,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Afișează sau ascunde lista", openViews: "Deschide lista de vizualizări", expandList: "Extinde lista", collapseList: "Restrânge lista", collapse: "Restrânge", home: "Înapoi la pagina principală",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "Plimbări de rugăciune", 'tab.upg-neighbor': "Vecin UPG", 'tab.key-cities': "Orașe-cheie", 'tab.all-people-groups': "Toate popoarele", 'tab.pplr-data-maps': "Hărți de date PPLR", 'tab.wall-of-people-groups': "Zidul popoarelor", 'tab.country-posters': "Afișe pe țări", 'sub.country-posters': "un afiș pentru fiecare țară", 'tab.doxa-vision-card': "Card de viziune Doxa",
'tab.prayer-walk-planner': "Plimbări de rugăciune", 'tab.upg-neighbor': "Vecin UPG", 'tab.key-cities': "Orașe-cheie", 'tab.all-people-groups': "Toate popoarele", 'tab.pplr-data-maps': "Hărți de date PPLR", 'tab.wall-of-people-groups': "Zidul popoarelor", 'tab.country-posters': "Afișe pe țări", 'sub.country-posters': "un afiș pentru fiecare țară", 'tab.poster-design': "Design de afișe", 'sub.poster-design': "toate stilurile și dimensiunile de afiș", 'tab.doxa-vision-card': "Card de viziune Doxa",
},
ru: {
backToTop: 'Наверх',
@ -183,7 +183,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Показать или скрыть список", openViews: "Открыть список представлений", expandList: "Развернуть список", collapseList: "Свернуть список", collapse: "Свернуть", home: "На главную страницу",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "Молитвенные прогулки", 'tab.upg-neighbor': "Сосед UPG", 'tab.key-cities': "Ключевые города", 'tab.all-people-groups': "Все народы", 'tab.pplr-data-maps': "Карты данных PPLR", 'tab.wall-of-people-groups': "Стена народов", 'tab.country-posters': "Плакаты по странам", 'sub.country-posters': "плакат для каждой страны", 'tab.doxa-vision-card': "Карточка видения Doxa",
'tab.prayer-walk-planner': "Молитвенные прогулки", 'tab.upg-neighbor': "Сосед UPG", 'tab.key-cities': "Ключевые города", 'tab.all-people-groups': "Все народы", 'tab.pplr-data-maps': "Карты данных PPLR", 'tab.wall-of-people-groups': "Стена народов", 'tab.country-posters': "Плакаты по странам", 'sub.country-posters': "плакат для каждой страны", 'tab.poster-design': "Дизайн плакатов", 'sub.poster-design': "все стили и размеры плакатов", 'tab.doxa-vision-card': "Карточка видения Doxa",
},
ar: {
backToTop: 'إلى الأعلى',
@ -204,7 +204,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "إظهار القائمة أو إخفاؤها", openViews: "فتح قائمة العروض", expandList: "توسيع القائمة", collapseList: "طي القائمة", collapse: "طي", home: "العودة إلى الصفحة الرئيسية",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "جولات الصلاة", 'tab.upg-neighbor': "جار UPG", 'tab.key-cities': "المدن الرئيسية", 'tab.all-people-groups': "جميع الشعوب", 'tab.pplr-data-maps': "خرائط بيانات PPLR", 'tab.wall-of-people-groups': "جدار الشعوب", 'tab.country-posters': "ملصقات الدول", 'sub.country-posters': "ملصق لكل دولة", 'tab.doxa-vision-card': "بطاقة رؤية Doxa",
'tab.prayer-walk-planner': "جولات الصلاة", 'tab.upg-neighbor': "جار UPG", 'tab.key-cities': "المدن الرئيسية", 'tab.all-people-groups': "جميع الشعوب", 'tab.pplr-data-maps': "خرائط بيانات PPLR", 'tab.wall-of-people-groups': "جدار الشعوب", 'tab.country-posters': "ملصقات الدول", 'sub.country-posters': "ملصق لكل دولة", 'tab.poster-design': "تصميم الملصقات", 'sub.poster-design': "كل أنماط الملصقات وأحجامها", 'tab.doxa-vision-card': "بطاقة رؤية Doxa",
},
hi: {
backToTop: 'ऊपर जाएँ',
@ -225,7 +225,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "सूची दिखाएँ या छिपाएँ", openViews: "दृश्यों की सूची खोलें", expandList: "सूची फैलाएँ", collapseList: "सूची संक्षिप्त करें", collapse: "संक्षिप्त करें", home: "होमपेज पर वापस जाएँ",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "प्रार्थना पदयात्रा", 'tab.upg-neighbor': "UPG पड़ोसी", 'tab.key-cities': "प्रमुख शहर", 'tab.all-people-groups': "सभी जनसमूह", 'tab.pplr-data-maps': "PPLR डेटा मानचित्र", 'tab.wall-of-people-groups': "जनसमूहों की दीवार", 'tab.country-posters': "देश पोस्टर", 'sub.country-posters': "हर देश के लिए एक पोस्टर", 'tab.doxa-vision-card': "Doxa दर्शन कार्ड",
'tab.prayer-walk-planner': "प्रार्थना पदयात्रा", 'tab.upg-neighbor': "UPG पड़ोसी", 'tab.key-cities': "प्रमुख शहर", 'tab.all-people-groups': "सभी जनसमूह", 'tab.pplr-data-maps': "PPLR डेटा मानचित्र", 'tab.wall-of-people-groups': "जनसमूहों की दीवार", 'tab.country-posters': "देश पोस्टर", 'sub.country-posters': "हर देश के लिए एक पोस्टर", 'tab.poster-design': "पोस्टर डिज़ाइन", 'sub.poster-design': "हर पोस्टर शैली और आकार", 'tab.doxa-vision-card': "Doxa दर्शन कार्ड",
},
zh: {
backToTop: '回到顶部',
@ -246,7 +246,7 @@ export const STRINGS = {
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "显示或隐藏列表", openViews: "打开视图列表", expandList: "展开列表", collapseList: "折叠列表", collapse: "折叠", home: "返回首页",
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
'tab.prayer-walk-planner': "祷告行走", 'tab.upg-neighbor': "UPG 邻居", 'tab.key-cities': "重点城市", 'tab.all-people-groups': "所有民族", 'tab.pplr-data-maps': "PPLR 数据地图", 'tab.wall-of-people-groups': "民族之墙", 'tab.country-posters': "国家海报", 'sub.country-posters': "每个国家一张海报", 'tab.doxa-vision-card': "Doxa 异象卡",
'tab.prayer-walk-planner': "祷告行走", 'tab.upg-neighbor': "UPG 邻居", 'tab.key-cities': "重点城市", 'tab.all-people-groups': "所有民族", 'tab.pplr-data-maps': "PPLR 数据地图", 'tab.wall-of-people-groups': "民族之墙", 'tab.country-posters': "国家海报", 'sub.country-posters': "每个国家一张海报", 'tab.poster-design': "海报设计", 'sub.poster-design': "所有海报样式与尺寸", 'tab.doxa-vision-card': "Doxa 异象卡",
}
}

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>doxa-maps · poster-design staging</title>
<link rel="icon" href="data:,">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: #f3f3f1; color: #3b463d; min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; }
.wrap { max-width: 1280px; margin: 0 auto; padding: 32px 20px; }
h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
.sub { color: #6d766e; font-size: 13px; margin: 6px 0 24px; }
.slot { border: 1px solid #d1d4cd; border-radius: 12px; overflow: hidden; background: #fff; margin-bottom: 24px; }
.slot .label { font: 12px/1 ui-monospace, monospace; color: #6d766e; padding: 8px 12px;
background: #f3f3f1; border-bottom: 1px solid #d1d4cd; }
.frame { height: 760px; position: relative; }
poster-design { display: block; width: 100%; height: 100%; }
</style>
</head>
<body>
<div class="wrap">
<h1>poster-design · staging</h1>
<p class="sub">One bundle, three flat profiles: the two-tab element and each of its tabs alone.
All compile into <code>poster-design.js</code>.</p>
<div class="slot">
<div class="label">profile-config.profile = "poster-design-mfe"</div>
<div class="frame"><poster-design profile-config='{"profile":"poster-design-mfe"}'></poster-design></div>
</div>
<div class="slot">
<div class="label">profile-config.profile = "poster-styles"</div>
<div class="frame"><poster-design profile-config='{"profile":"poster-styles"}'></poster-design></div>
</div>
</div>
<script type="module" src="./index.js"></script>
</body>
</html>

View file

@ -0,0 +1,23 @@
/**
* plugin-designs/poster-design/index.js: the Poster Design bundle entry.
*
* One tag, <poster-design>, and three profiles in this folder:
* poster-design-mfe the two-tab element (Styles + Sizes), the one a gallery mounts;
* poster-styles every poster template, one card per configuration;
* poster-sizes every poster size.
*
* <script src="…/poster-design/poster-design.js"></script>
* <poster-design profile-config='{"profile":"poster-design-mfe"}'></poster-design>
*/
import { defineMapElement } from '@map/elements/defineMapElement.js'
// Flat: './*.vue' finds the profiles beside this file (src/ holds private components, not profiles).
const profileModules = import.meta.glob('./*.vue')
const PosterDesignElement = defineMapElement({ profileModules })
if (typeof customElements !== 'undefined' && !customElements.get('poster-design')) {
customElements.define('poster-design', PosterDesignElement)
}
export default PosterDesignElement

View file

@ -0,0 +1,9 @@
{
"type": "nested-elements-mfe",
"label": "Poster Design — Styles + Sizes",
"composes": [
"poster-design/poster-styles",
"poster-design/poster-sizes"
],
"note": "The two-tab Poster Design element, the same shape as country-posters-mfe: the shared ViewSelect over Styles at [0,0] and Sizes at [1,0], each pane its own element. Styles renders the library's PosterWall with two host seams (headerAction for Show full details, previewSrc for thumbnails drawn from the library). Sizes reads the one shared size table."
}

View file

@ -0,0 +1,124 @@
<script setup>
/**
* poster-design-mfe.vue: Poster Design as ONE two-tab element, Styles and Sizes.
*
* @element poster-design
* @description Poster Design: every poster template (Styles) and every poster size (Sizes) side by side,
* drawn from the library, each named by its configuration so it can be handed to an agent.
*
* The same shape as country-posters-mfe and upg-100-mfe, on purpose: the pane-wrapper layout, the
* library's ViewSelect for the tabs, each pane its own element with its own ProfileLoader, and one
* ShareButton sharing THIS composite. Nothing here is poster-specific; the two tabs are the two profiles
* poster-styles and poster-sizes, each also embeddable alone.
*/
import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue'
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'
import { useOptionalT } from '@map/composables/useOptionalT.js'
const t = useOptionalT('')
const cfg = inject('profileConfig', ref(null))
const uiStore = inject('uiStore', null)
const isDark = computed(() => !!uiStore?.isDark)
const TAG = 'poster-design'
const instanceId = computed(() => cfg.value?.instanceId || 'poster-design-mfe')
const spec = computed(() => ({
id: 'poster-design-mfe',
tag: TAG,
config: { instanceId: instanceId.value },
// ORDER LIVES IN at[0]: buildPaneColumn sorts by it while ViewSelect renders this array as written,
// so the two must agree.
panes: [
{ id: 'styles', label: t('posterDesign.tab.styles'), at: [0, 0], config: { profile: 'poster-styles' } },
{ id: 'sizes', label: t('posterDesign.tab.sizes'), at: [1, 0], config: { profile: 'poster-sizes' } },
],
paneHint: false,
}))
const stage = ref(null)
const view = ref(cfg.value?.view === 'sizes' ? 'sizes' : 'styles')
let column = null
let offScroll = null
// Page-level keys a pane inherits from this element's host, by KEY LIST (a pane's own profile and
// instanceId always win, since pane.config is spread last).
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'tokenUrl', 'dataSource', 'locale', 'lang', 'isDark', 'provider', 'list']
function paneElement(pane) {
const el = document.createElement(TAG)
const host = cfg.value || {}
const inherited = {}
for (const k of INHERITED_PAGE_KEYS) if (host[k] !== undefined && host[k] !== null && host[k] !== '') inherited[k] = host[k]
if (inherited.locale === undefined && inherited.lang !== undefined) inherited.locale = inherited.lang
el.setAttribute('profile-config', JSON.stringify({ ...inherited, ...spec.value.config, ...pane.config }))
return el
}
// `inert` on the pane the reader is not looking at, as the other two-tab composites do: the panes are
// stacked in one scroll column, so the hidden one is still full-size geometry.
function markActivePane(activeId) {
if (!column || !activeId) return
for (const pane of column.panes) {
const el = column.mountEl(pane.id)
if (!el) continue
if (pane.id === activeId) el.removeAttribute('inert')
else el.setAttribute('inert', '')
}
}
function go(id) {
view.value = id
column?.scrollTo(id)
markActivePane(id)
}
onMounted(() => {
if (!stage.value) return
column = buildPaneColumn(spec.value, { host: stage.value })
if (!column) return
for (const pane of column.panes) if (pane.config) column.mountEl(pane.id)?.appendChild(paneElement(pane))
markActivePane(column.current() || view.value)
offScroll = column.onScroll(() => {
const cur = column.current()
if (cur && cur !== view.value) view.value = cur
if (cur) markActivePane(cur)
})
if (view.value !== 'styles') go(view.value)
})
onBeforeUnmount(() => { offScroll?.(); column?.destroy(); column = null })
const SHARE_EMBED_PATH = 'poster-design/poster-design-mfe/index.html'
useShadowStyles(PANE_GRID_STYLES + `
:host{ display:block; width:100%; height:100%; min-height:420px; }
.pdmfe{
--ui-bg:#ffffff; --ui-bg-elevated:#f3f3f1; --ui-bg-accented:#f3f3f1;
--ui-border:#d1d4cd; --ui-text:#3b463d; --ui-text-muted:#6d766e; --ui-primary:#92b195;
--header:56px;
position:absolute; inset:0; display:flex; flex-direction:column; min-height:0;
background:var(--ui-bg); color:var(--ui-text);
font:14px/1.4 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,system-ui,sans-serif;
}
.pdmfe .hdr{
flex:0 0 var(--header); height:var(--header); position:relative;
display:flex; align-items:center; justify-content:flex-end; padding:0 12px;
background:var(--ui-bg); border-bottom:1px solid var(--ui-border);
}
.viewsel{ position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); }
.pdmfe .stage{ position:relative; flex:1 1 auto; min-height:0; }
.pane > .pane-mount > *{ position:absolute; inset:0; width:100%; height:100%; display:block; }
`, 'poster-design-mfe')
</script>
<template>
<div class="pdmfe">
<header class="hdr">
<ViewSelect :views="spec.panes" :current="view" :legend="t('tabs.selectView')" :label="t('tabs.views')" @select="go" />
<ShareButton :is-dark="isDark" :embed-path="SHARE_EMBED_PATH" />
</header>
<div class="stage" ref="stage"></div>
</div>
</template>

View file

@ -0,0 +1,5 @@
{
"type": "single",
"label": "Poster Design — Sizes",
"note": "Every poster size from the shared size table (library/constants/posterSizes.js), each as a print preview; also embeddable alone."
}

View file

@ -0,0 +1,35 @@
<script setup>
/**
* poster-sizes.vue: every poster size, each card a print preview at that size.
*
* @element poster-design
* @description Every poster size the print picker offers (standard paper up to 5 × 8 ft), each drawn as a
* print preview, portrait only, from the one shared size table.
*
* The sizes come from the shared size table in library/constants/posterSizes.js and nowhere else: a
* second list here would drift from the print picker. Until that table carries the size ladder, this
* tab says so rather than inventing one.
*/
import { computed } from 'vue'
import { useOptionalT } from '@map/composables/useOptionalT.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
const t = useOptionalT('')
const heading = computed(() => t('posterDesign.sizes.heading'))
const waiting = computed(() => t('posterDesign.sizes.waiting'))
useShadowStyles(`
:host{display:block;width:100%;height:100%;min-height:420px}
.pd-sizes{position:absolute;inset:0;overflow-y:auto;padding:28px 20px;background:#f3f3f1;color:#3b463d;
font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,system-ui,sans-serif}
.pd-sizes__h{margin:0 0 8px;font-size:22px;font-weight:700}
.pd-sizes__note{margin:0;max-width:60ch;color:#6d766e}
`, 'poster-design-sizes')
</script>
<template>
<div class="pd-sizes">
<h2 class="pd-sizes__h">{{ heading }}</h2>
<p class="pd-sizes__note">{{ waiting }}</p>
</div>
</template>

View file

@ -0,0 +1,5 @@
{
"type": "single",
"label": "Poster Design — Styles",
"note": "Every poster template, one card per configuration (src/templates.js), drawn from the library with sample data; also embeddable alone."
}

View file

@ -0,0 +1,145 @@
<script setup>
/**
* poster-styles.vue: every poster template the library prints, one card per configuration.
*
* @element poster-design
* @description Every poster template in one grid (map sheets, the prayer packet, prayer cards, the wall
* panels, the map print), each card drawn from the library with sample data and named by its
* configuration, with "Show full details" for what an agent needs to reuse it.
*
* The cards ARE the poster wall's cards: this profile renders the library's PosterWall and changes two
* things through its host seams. headerAction turns the card's "Show on map" sticker into "Show full
* details" (there is no map here), and previewSrc draws each thumbnail from the library at view time
* instead of reading a stored image, so a change to a template shows up here the next time it opens.
* The catalogue itself is src/templates.js.
*/
import { computed, inject, ref } from 'vue'
import PosterWall from '@map/components/poster/PosterWall.vue'
import { useOptionalT } from '@map/composables/useOptionalT.js'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
import { getApiBaseUrl } from '@map/api/apiBaseUrl.js'
import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
import countryData from '@map/data/country-posters.json'
import { makeCountrySheet } from '@map/packet/countrySheet.js'
import { listConfigurations, FAMILY_KEYS, SAMPLE_COUNTRY_ID } from './src/templates.js'
import FullDetailsDialog from './src/FullDetailsDialog.vue'
const profileConfig = inject('profileConfig', ref(null))
const instanceId = computed(() => profileConfig.value?.instanceId || 'poster-design-styles')
const assetsBase = computed(() => resolveAssetsBase(profileConfig.value))
const t = useOptionalT('')
const configs = listConfigurations()
const byName = new Map(configs.map((c) => [c.name, c]))
// A card's title: the template's name, then its size. Card stocks name the stock the way the wall's own
// stock picker does (the noun translates, the size is printed as a printer's box prints it).
function stockLabel(s) {
if (!s.noun) return s.label
const noun = s.noun === 'postcard' ? t('wall.stockPostcard') : t('wall.stockCard')
const dims = `${s.w} × ${s.h} in`
return s.side ? `${noun} ${dims} (${t('wall.stockLandscape')})` : `${noun} ${dims}`
}
const labelOf = (c) => (c.stock ? `${t(c.labelKey)}: ${stockLabel(c.stock)}` : `${t(c.labelKey)}: ${c.size}`)
// No `count`: these are templates, not lists of people, so the sticker reads "Show full details" alone.
const items = computed(() => configs.map((c) => ({ id: c.name, label: labelOf(c) })))
const groupBy = (item) => t(FAMILY_KEYS[byName.get(item.id)?.family] || FAMILY_KEYS.mapSheet)
// ── drawing a configuration from the library ──────────────────────────────────────────────────
const sample = (countryData.countries || []).find((c) => c.id === SAMPLE_COUNTRY_ID)
const recordsFor = createPeopleGroupRecords({
apiBase: () => profileConfig.value?.apiBaseUrl || getApiBaseUrl(),
lang: () => getRuntimeLocale() || detectLocale(),
profileConfig,
})
// The ONE path every surface draws a country sheet through (the Country Posters wall, the Regions tab),
// so the sample carries its region hue and its "map N of M" exactly as the wall prints it.
const sheetFor = makeCountrySheet({ records: recordsFor, assetsBase: () => assetsBase.value, countries: countryData.countries || [] })
const countrySheet = (format) => (sample ? sheetFor(sample, format) : Promise.resolve(''))
const ctx = { countrySheet }
// The printed document's size, measured when it is drawn: what "file size" means in the details.
const fileBytes = ref({})
async function renderDocument(name) {
const c = byName.get(name)
if (!c || !c.render) return ''
const html = await c.render(ctx)
if (html) fileBytes.value = { ...fileBytes.value, [name]: new Blob([html]).size }
return html || ''
}
// previewSrc: the thumbnail, rasterised from the drawn document at a quarter scale (a 1500px sheet
// becomes ~375px, the card's size). A render that comes out blank is refused, so the card shows
// "preview unavailable" rather than a black rectangle.
async function previewSrc(item) {
const html = await renderDocument(item.id)
if (!html) return null
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js')
const blob = await rasterPoster(html, { scale: 0.25 })
if (!(await looksRendered(blob))) return null
return URL.createObjectURL(blob)
}
// The card's print button prints THIS configuration: the format the wall asks for is ignored on
// purpose, because the card already names its size.
const sheet = (item) => renderDocument(item.id)
// ── "Show full details" ───────────────────────────────────────────────────────────────────────
const open = ref(null)
const openLabel = computed(() => (open.value ? labelOf(open.value) : ''))
function showDetails(item) { open.value = byName.get(item.id) || null }
const heading = computed(() => t('posterDesign.styles.heading'))
// PosterWall renders the intro as HTML, so plain catalogue text is escaped first.
const escHtml = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
const intro = computed(() => escHtml(t('posterDesign.styles.intro')))
// Child components' <style> blocks are dropped by defineCustomElement, so this profile carries the
// dialog's CSS too. The floor sits on :host, where a shell that sizes its pane can override it.
useShadowStyles(`
:host{display:block;width:100%;height:100%;min-height:420px}
.pd-styles{position:absolute;inset:0;min-height:0}
.pd-dialog{box-sizing:border-box;border:1px solid #d1d4cd;border-radius:12px;padding:20px 22px;max-width:min(560px,calc(100vw - 32px));
width:100%;color:#3b463d;background:#fff;font:14px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,system-ui,sans-serif;
box-shadow:0 12px 40px rgba(8,12,16,.25)}
.pd-dialog::backdrop{background:rgba(8,12,16,.55)}
.pd-dialog__title{margin:0 0 14px;font-size:18px;font-weight:700;line-height:1.3}
.pd-dialog__list{display:grid;grid-template-columns:max-content 1fr;gap:8px 16px;margin:0}
.pd-dialog__list dt{color:#6d766e;font-weight:600}
.pd-dialog__list dd{margin:0;min-width:0;overflow-wrap:anywhere}
.pd-dialog__list code{font:12.5px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;background:#f3f3f1;border-radius:4px;padding:1px 5px}
.pd-dialog__name{font-weight:700}
.pd-dialog__copy,.pd-dialog__close{font:inherit;border-radius:8px;border:1px solid #d1d4cd;background:#f3f3f1;color:#3b463d;
padding:4px 10px;cursor:pointer;margin-inline-start:8px}
.pd-dialog__note{margin:14px 0 0;color:#6d766e}
.pd-dialog__agent{margin:14px 0 0;padding:10px 12px;border-radius:8px;background:#f3f3f1;border-inline-start:4px solid #92b195}
.pd-dialog__actions{display:flex;justify-content:flex-end;margin:16px 0 0}
.pd-dialog__close{margin:0;background:#92b195;border-color:#92b195;color:#fff;font-weight:600;padding:6px 16px}
@media (max-width:480px){.pd-dialog__list{grid-template-columns:1fr}.pd-dialog__list dd{margin-bottom:6px}}
`, 'poster-design-styles')
</script>
<template>
<div class="pd-styles">
<PosterWall
:items="items"
:group-by="groupBy"
:instance-id="instanceId"
:assets-base="assetsBase"
:heading="heading"
:intro="intro"
unit=""
:collapsible="true"
:header-action="{ labelKey: 'posterDesign.showFullDetails', ariaKey: 'posterDesign.showFullDetailsAria' }"
:preview-src="previewSrc"
:sheet="sheet"
:hide-prayer-list-when-soon="true"
download-prefix="doxa-poster-design"
@header-action="showDetails"
/>
<FullDetailsDialog :config="open" :label="openLabel" :file-bytes="open ? (fileBytes[open.name] ?? null) : null" @close="open = null" />
</div>
</template>

View file

@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* check-catalogue.mjs (check:poster-design-catalogue): Poster Design's configuration catalogue
* (plugin-designs/poster-design/src/templates.js) says only true things about the library.
*
* 1. SHEET_FORMATS has exactly the formats poster-render.js FORMATS defines, at the same pixel size.
* The generator is served as a pinned asset, not bundled, so the catalogue cannot import the table;
* this is what keeps the two from drifting.
* 2. Every configuration name is unique and shaped like a name (lowercase words, dots, dashes, digits):
* a name is what a person hands an agent, so two cards must never share one.
* 3. The card entries are exactly card-render.js STOCK, one per stock.
* 4. Every catalogue key an entry names (labelKey, missing, the family keys) exists in the English
* posterDesign catalogue.
* 5. The sample country exists in the country data and can be drawn (has geometry, a sheet tier).
* A catalogue that lists nothing FAILS (vacuous guard).
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
const failures = []
const fail = (m) => failures.push(m)
const { createServer } = await import('vite')
const server = await createServer({
root: ROOT, configFile: false, logLevel: 'error', appType: 'custom',
server: { middlewareMode: true, hmr: false, ws: false, watch: null },
resolve: { alias: { '@map': join(ROOT, 'library') } },
})
let cat, STOCK
try {
cat = await server.ssrLoadModule('/plugin-designs/poster-design/src/templates.js')
;({ STOCK } = await server.ssrLoadModule('/library/packet/card-render.js'))
} finally { await server.close() }
const configs = cat.listConfigurations()
if (!configs.length) fail('vacuous: listConfigurations() returned nothing')
// 1. formats against the generator's own table
const src = readFileSync(join(ROOT, 'library/packet/poster-render.js'), 'utf8')
const block = (src.match(/const FORMATS = \{([\s\S]*?)\n\};?/) || [])[1]
if (!block) fail('poster-render.js: could not find "const FORMATS = {"; update this check')
else {
const found = {}
for (const m of block.matchAll(/^\s*'?([\w]+)'?\s*:\s*\{\s*w:\s*(\d+),\s*h:\s*(\d+)/gm)) found[m[1]] = [Number(m[2]), Number(m[3])]
const names = Object.keys(found)
if (names.length < 3) fail(`poster-render.js FORMATS: parsed only ${names.length} format(s) (${names.join(', ')}); the parser no longer matches`)
for (const k of names) {
const mine = cat.SHEET_FORMATS[k]
if (!mine) fail(`FORMATS has '${k}' and the catalogue does not`)
else if (mine.px[0] !== found[k][0] || mine.px[1] !== found[k][1]) fail(`'${k}': catalogue says ${mine.px.join('×')}, FORMATS says ${found[k].join('×')}`)
}
for (const k of Object.keys(cat.SHEET_FORMATS)) if (!found[k]) fail(`the catalogue has format '${k}', which FORMATS does not`)
}
// 2. names
const seen = new Set()
for (const c of configs) {
if (!/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/i.test(c.name)) fail(`'${c.name}' is not a configuration name (words joined by . or -)`)
if (seen.has(c.name)) fail(`two configurations are named '${c.name}'`)
seen.add(c.name)
}
// 3. stocks
const stockNames = configs.filter((c) => c.family === 'cards').map((c) => c.settings.stock).sort()
const want = Object.keys(STOCK).sort()
if (JSON.stringify(stockNames) !== JSON.stringify(want)) fail(`card configurations ${JSON.stringify(stockNames)} are not card-render STOCK ${JSON.stringify(want)}`)
// 4. keys exist in English
const en = JSON.parse(readFileSync(join(ROOT, 'library/i18n/locales/en/posterDesign.json'), 'utf8'))
const has = (key) => {
const parts = String(key).split('.')
if (parts.shift() !== 'posterDesign') return false
let o = en
for (const p of parts) { if (!o || typeof o !== 'object' || !(p in o)) return false; o = o[p] }
return typeof o === 'string'
}
const keys = [...configs.flatMap((c) => [c.labelKey, c.missing].filter(Boolean)), ...Object.values(cat.FAMILY_KEYS)]
for (const k of keys) if (!has(k)) fail(`catalogue key '${k}' is not in en/posterDesign.json`)
for (const c of configs) if (!cat.FAMILY_KEYS[c.family]) fail(`'${c.name}' has family '${c.family}', which has no heading key`)
// 5. the sample
const data = JSON.parse(readFileSync(join(ROOT, 'library/data/country-posters.json'), 'utf8'))
const s = (data.countries || []).find((c) => c.id === cat.SAMPLE_COUNTRY_ID)
if (!s) fail(`sample '${cat.SAMPLE_COUNTRY_ID}' is not in library/data/country-posters.json`)
else if (!s.hasGeometry || !s.sheet || s.unsupportedReason) fail(`sample '${s.id}' cannot be drawn (hasGeometry ${s.hasGeometry}, sheet ${s.sheet})`)
if (failures.length) {
console.error(`POSTER-DESIGN-CATALOGUE: FAIL (${failures.length})\n - ${failures.join('\n - ')}`)
process.exitCode = 1
} else {
console.log(`POSTER-DESIGN-CATALOGUE: PASS — ${configs.length} configurations, ${Object.keys(cat.SHEET_FORMATS).length} sheet formats matching poster-render FORMATS, ${want.length} card stocks from STOCK, ${keys.length} catalogue keys present, sample ${cat.SAMPLE_COUNTRY_ID}`)
}

View file

@ -0,0 +1,83 @@
<script setup>
/**
* FullDetailsDialog.vue: "Show full details" for one poster configuration.
*
* Everything a person needs to name the template to an agent, or an agent needs to find it: the
* configuration name (the stable id), what it is, its size, the size of the file it prints, where its
* renderer lives, its settings and its sample data.
*
* A NATIVE <dialog> opened with showModal(), so it sits in the browser's TOP LAYER. A fixed scrim inside
* this element would be captured by a host's containing block (VerticalTabsLayout sets contain:layout on
* its content pane) and would fight the shell's z-index ladder; the top layer is above both, and brings
* Escape-to-close and focus return with it.
*/
import { ref, watch, nextTick, computed } from 'vue'
import { useOptionalT } from '@map/composables/useOptionalT.js'
const props = defineProps({
// One entry of listConfigurations(), or null when closed.
config: { type: Object, default: null },
// The printed document's size in bytes, once it has been drawn; null until then.
fileBytes: { type: Number, default: null },
label: { type: String, default: '' },
})
const emit = defineEmits(['close'])
const t = useOptionalT('')
const dlg = ref(null)
watch(() => props.config, async (c) => {
await nextTick()
const d = dlg.value
if (!d) return
if (c && !d.open) d.showModal?.()
else if (!c && d.open) d.close()
})
// A number with its unit, or an identifier, inside Arabic text is a left-to-right RUN: without an
// isolate the bidi algorithm reorders it ("KB 888", a configuration name split at its dots).
// LRI … PDI (U+2066 … U+2069) keeps the run whole in any locale and changes nothing in an LTR one.
const ltr = (s) => `\u2066${s}\u2069`
const fileSize = computed(() => {
const b = props.fileBytes
if (b == null) return t('posterDesign.details.fileSizeUnknown')
return ltr(b >= 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(b / 1024))} KB`)
})
const settings = computed(() => {
const s = props.config?.settings || {}
return Object.keys(s).length ? Object.entries(s).map(([k, v]) => `${k}: ${v}`).join(' · ') : '—'
})
const agentLine = computed(() => (props.config ? t('posterDesign.details.forAgents', { name: ltr(props.config.name) }) : ''))
async function copyName() {
try { await navigator.clipboard.writeText(props.config.name) } catch (_) { /* the name stays selectable */ }
}
</script>
<template>
<dialog ref="dlg" class="pd-dialog" aria-labelledby="pd-dialog-title" @close="emit('close')" @cancel="emit('close')">
<template v-if="config">
<h2 id="pd-dialog-title" class="pd-dialog__title">{{ label }}</h2>
<dl class="pd-dialog__list">
<dt>{{ t('posterDesign.details.configName') }}</dt>
<dd><code dir="ltr" class="pd-dialog__name">{{ config.name }}</code>
<button type="button" class="pd-dialog__copy" @click="copyName">{{ t('posterDesign.details.copyName') }}</button></dd>
<dt>{{ t('posterDesign.details.size') }}</dt>
<dd dir="ltr">{{ config.size }}<span v-if="config.px"> · {{ config.px[0] }} × {{ config.px[1] }} px</span></dd>
<dt>{{ t('posterDesign.details.fileSize') }}</dt>
<dd>{{ fileSize }}</dd>
<dt>{{ t('posterDesign.details.settings') }}</dt>
<dd dir="ltr">{{ settings }}</dd>
<dt>{{ t('posterDesign.details.sample') }}</dt>
<dd dir="ltr">{{ config.sample || '—' }}</dd>
<dt>{{ t('posterDesign.details.renderer') }}</dt>
<dd dir="ltr"><code>{{ config.renderer }}</code></dd>
</dl>
<p v-if="config.legacy" class="pd-dialog__note">{{ t('posterDesign.details.legacy') }}</p>
<p v-if="config.missing" class="pd-dialog__note">{{ t(config.missing) }}</p>
<p class="pd-dialog__agent">{{ agentLine }}</p>
<form method="dialog" class="pd-dialog__actions">
<button type="submit" class="pd-dialog__close">{{ t('buttons.close') }}</button>
</form>
</template>
</dialog>
</template>

View file

@ -0,0 +1,109 @@
/**
* templates.js: every poster template the library can print, one entry per CONFIGURATION.
*
* A configuration is one template at one setting (a map sheet at 3 × 3, a packet in its lightweight
* variant, a card deck on postcard stock). Its NAME is the stable id a person gives an agent ("use
* map-sheet.country.3x3"), so a name is never renamed once published; a retired configuration keeps
* its name and is marked legacy.
*
* Each entry says where its renderer lives in library/ and, when the gallery can draw it, how:
* render(ctx) returns the template's HTML document drawn from the LIBRARY with the sample data, so a
* library change shows up in the gallery the next time it opens. Entries the gallery cannot draw yet
* say so (render: null) and the card shows the wall's "preview unavailable" placeholder; the details
* dialog states which part is missing, rather than a picture of something else.
*
* Where the library already has the list (card stocks), it is DERIVED here, never retyped.
*/
import { STOCK } from '@map/packet/card-render.js'
// The sample every map sheet is drawn with: one real country from the data the country wall ships,
// mid-sized (34 groups), on the standard sheet tier, with full outlines, so each sheet shows every part.
export const SAMPLE_COUNTRY_ID = 'country-npl'
// The library's poster formats (library/packet/poster-render.js FORMATS). The generator is served as a
// pinned asset, not bundled, so its table cannot be imported here; check:poster-design-catalogue reads
// FORMATS from the source and fails when the two disagree.
export const SHEET_FORMATS = {
'3x3': { page: '3 × 3', px: [1500, 1500] },
'3x4': { page: '3 × 4', px: [1500, 2000], legacy: true },
letter: { page: 'Letter, portrait', px: [1500, 1941] },
packet: { page: 'Letter, portrait (packet page 1)', px: [1500, 1941] },
}
// Families are the sections of the Styles tab, in reading order. The keys are written out, never
// assembled, so the catalogue checks and the i18n area build can see each one.
export const FAMILY_KEYS = {
mapSheet: 'posterDesign.family.mapSheet',
packet: 'posterDesign.family.packet',
cards: 'posterDesign.family.cards',
wall: 'posterDesign.family.wall',
mapPrint: 'posterDesign.family.mapPrint',
}
function sheetEntry(format) {
const f = SHEET_FORMATS[format]
return {
name: format === 'packet' ? 'map-sheet.packet-page' : `map-sheet.country.${format}`,
family: 'mapSheet',
labelKey: format === 'packet' ? 'posterDesign.tpl.mapSheetPacketPage' : 'posterDesign.tpl.mapSheetCountry',
size: f.page,
px: f.px,
legacy: !!f.legacy,
renderer: 'library/packet/posterSheets.js generateCountrySheet → library/packet/poster-render.js',
settings: { format },
sample: SAMPLE_COUNTRY_ID,
render: (ctx) => ctx.countrySheet(format),
}
}
/** @returns {Array<object>} every configuration, in the gallery's order. */
export function listConfigurations() {
return [
...Object.keys(SHEET_FORMATS).map(sheetEntry),
...['full', 'lightweight'].map((variant) => ({
name: `packet.${variant}`,
family: 'packet',
labelKey: variant === 'full' ? 'posterDesign.tpl.packetFull' : 'posterDesign.tpl.packetLightweight',
size: 'Letter, portrait',
renderer: 'library/packet/packet-render.js buildPacket (VARIANTS)',
settings: { variant },
sample: SAMPLE_COUNTRY_ID,
render: null,
missing: 'posterDesign.missing.packet',
})),
...Object.entries(STOCK).map(([id, s]) => ({
name: `cards.${id}`,
family: 'cards',
labelKey: 'posterDesign.tpl.cards',
stock: s,
size: `${s.w} × ${s.h} in`,
renderer: 'library/packet/card-render.js createCardRenderer (STOCK)',
settings: { stock: id },
sample: SAMPLE_COUNTRY_ID,
render: null,
missing: 'posterDesign.missing.cards',
})),
{
name: 'wall.bless-panels',
family: 'wall',
labelKey: 'posterDesign.tpl.blessPanels',
size: '5 × 8 ft, 3 panels',
renderer: 'plugin-designs/wall-of-people-groups/src/blessPoster.js (not in library/ yet)',
settings: {},
sample: null,
render: null,
missing: 'posterDesign.missing.bless',
},
{
name: 'map-print.dialog',
family: 'mapPrint',
labelKey: 'posterDesign.tpl.mapPrint',
size: 'library/constants/posterSizes.js',
renderer: 'library/components/poster/PosterDialog.vue + library/composables/useMapPoster.js',
settings: {},
sample: null,
render: null,
missing: 'posterDesign.missing.mapPrint',
},
]
}

View file

@ -1,12 +1,11 @@
{
"_comment": "Declares the composability tier. 'nested-mfe-spa' = capability 5: an app shell whose single element <posterverse-app> renders a vertical-tab rail of whole poster plugins, each mounted as its own real element from the manifest (no iframes). The tier is DERIVED from `composes` at tree generation (composing a cap-4 makes this cap-5) and this `type` is checked against it. `composes` lists every row in src/tabs.js whose bundle exists today; a provisional row (poster-design, bible-posters) joins the list when its bundle lands, since an unresolved ref fails check:capability-derived.",
"_comment": "Declares the composability tier. 'nested-mfe-spa' = capability 5: an app shell whose single element <posterverse-app> renders a vertical-tab rail of whole poster plugins, each mounted as its own real element from the manifest (no iframes). The tier is DERIVED from `composes` at tree generation (composing a cap-4 makes this cap-5) and this `type` is checked against it. `composes` lists every row in src/tabs.js whose bundle exists; a provisional row (bible-posters) joins the list in the SAME push as its bundle, since an unresolved ref fails check:capability-derived and a missing one fails check:posterverse-shelf.",
"type": "nested-mfe-spa",
"composes": [
"my-upg-100-list/upg-100-mfe",
"country-posters/country-posters-mfe",
"wall-of-people-groups/wall-of-people-groups",
"all-people-groups/all-people-groups",
"upg-neighbor/upg-neighbor"
"poster-design/poster-design-mfe",
"wall-of-people-groups/wall-of-people-groups"
],
"label": "Posterverse (vertical-tab app shell: every poster plugin)"
}

View file

@ -9,7 +9,7 @@
*
* WHAT IT ASSERTS (each arm is poisoned by --self-test before a pass is trusted):
* A. the rail's rows are exactly the named sets: Posters = upg-100, country-posters, poster-design;
* Experimental = wall-of-people-groups, all-people-groups, upg-neighbor, bible-posters. Ids unique,
* Experimental = wall-of-people-groups, bible-posters (poster plugins only). Ids unique,
* every row in a declared section.
* B. no row names a NEVER bundle (key-cities, the shells themselves), and resolving the built
* manifest WITH a key-cities bundle planted in it yields no key-cities row (the real manifest
@ -34,7 +34,7 @@ const ROOT = resolve(PLUGIN, '../..')
const EXPECT = {
posters: ['upg-100', 'country-posters', 'poster-design'],
experimental: ['wall-of-people-groups', 'all-people-groups', 'upg-neighbor', 'bible-posters'],
experimental: ['wall-of-people-groups', 'bible-posters'],
}
const GATED = 'key-cities'
@ -114,7 +114,7 @@ function selfTest(base) {
const clone = (o) => JSON.parse(JSON.stringify(o))
const cases = [
['A: key-cities added to Experimental', (c) => { c.TABS = [...c.TABS, { id: 'key-cities', section: 'experimental', bundle: 'key-cities', profile: 'key-cities' }] }, /^A: section experimental|^B: row key-cities/],
['A: a row dropped', (c) => { c.TABS = c.TABS.filter((t) => t.id !== 'upg-neighbor') }, /^A: section experimental/],
['A: a row dropped', (c) => { c.TABS = c.TABS.filter((t) => t.id !== 'wall-of-people-groups') }, /^A: section experimental/],
['B: resolver that shows whatever the manifest carries', (c) => { c.resolveTabs = (m) => (m.bundles || []).map((b) => ({ id: b.bundle, bundle: b.bundle, profile: b.bundle, tag: b.bundle, scriptUrl: b.scriptUrl })) }, /^B: resolution produced a key-cities row/],
['C: a composite row falls back to a leaf', (c) => { c.manifest = clone(c.manifest); const b = c.manifest.bundles.find((x) => x.bundle === 'my-upg-100-list'); b.profiles = b.profiles.filter((p) => p.profile !== 'upg-100-mfe') }, /^C: row upg-100 opens/],
['D: composes drift', (c) => { c.meta = { ...c.meta, composes: c.meta.composes.slice(1) } }, /^D: the rail shows/],

View file

@ -18,8 +18,8 @@ export const STRINGS = {
'tab.upg-100': 'UPG 100 List Posters', 'sub.upg-100': 'my unreached list + posters',
'tab.country-posters': 'Country Posters', 'sub.country-posters': 'a poster for every country',
'tab.poster-design': 'Poster Design', 'sub.poster-design': 'every poster style and size',
'tab.wall-of-people-groups': 'Wall of People Groups', 'tab.all-people-groups': 'All People Groups',
'tab.upg-neighbor': 'UPG Neighbor', 'tab.bible-posters': 'Bible Posters',
'tab.wall-of-people-groups': 'Wall of People Groups',
'tab.bible-posters': 'Bible Posters',
experimentalSub: 'experimental bundle', lab: 'lab',
loading: 'Loading {label}…', loadFailed: 'Could not load {label}.',
manifestHint: 'This tab is composed from manifest.json — build the tree, or serve the built tree, to load it.',
@ -31,8 +31,8 @@ export const STRINGS = {
'tab.upg-100': 'Carteles de la lista UPG 100', 'sub.upg-100': 'mi lista de no alcanzados + carteles',
'tab.country-posters': 'Carteles por país', 'sub.country-posters': 'un cartel para cada país',
'tab.poster-design': 'Diseño de carteles', 'sub.poster-design': 'todos los estilos y tamaños de cartel',
'tab.wall-of-people-groups': 'Muro de los pueblos', 'tab.all-people-groups': 'Todos los pueblos',
'tab.upg-neighbor': 'Vecino UPG', 'tab.bible-posters': 'Carteles bíblicos',
'tab.wall-of-people-groups': 'Muro de los pueblos',
'tab.bible-posters': 'Carteles bíblicos',
experimentalSub: 'mapa experimental', lab: 'beta',
loading: 'Cargando {label}…', loadFailed: 'No se pudo cargar {label}.',
manifestHint: 'Esta pestaña se compone a partir de manifest.json: compila el árbol o sirve el árbol compilado para cargarla.',
@ -44,8 +44,8 @@ export const STRINGS = {
'tab.upg-100': 'Affiches de la liste UPG 100', 'sub.upg-100': 'ma liste des non atteints + affiches',
'tab.country-posters': 'Affiches par pays', 'sub.country-posters': 'une affiche pour chaque pays',
'tab.poster-design': 'Conception d’affiches', 'sub.poster-design': 'tous les styles et formats d’affiche',
'tab.wall-of-people-groups': 'Mur des peuples', 'tab.all-people-groups': 'Tous les peuples',
'tab.upg-neighbor': 'Voisin UPG', 'tab.bible-posters': 'Affiches bibliques',
'tab.wall-of-people-groups': 'Mur des peuples',
'tab.bible-posters': 'Affiches bibliques',
experimentalSub: 'carte expérimentale', lab: 'bêta',
loading: 'Chargement de {label}…', loadFailed: 'Impossible de charger {label}.',
manifestHint: "Cet onglet est composé à partir de manifest.json : construisez l'arborescence ou servez l'arborescence construite pour le charger.",
@ -57,8 +57,8 @@ export const STRINGS = {
'tab.upg-100': 'Poster der UPG 100 Liste', 'sub.upg-100': 'meine Liste der Unerreichten + Poster',
'tab.country-posters': 'Länderposter', 'sub.country-posters': 'ein Poster für jedes Land',
'tab.poster-design': 'Postergestaltung', 'sub.poster-design': 'alle Posterstile und -größen',
'tab.wall-of-people-groups': 'Wand der Volksgruppen', 'tab.all-people-groups': 'Alle Volksgruppen',
'tab.upg-neighbor': 'UPG-Nachbar', 'tab.bible-posters': 'Bibelposter',
'tab.wall-of-people-groups': 'Wand der Volksgruppen',
'tab.bible-posters': 'Bibelposter',
experimentalSub: 'experimentelle Karte', lab: 'Beta',
loading: '{label} wird geladen…', loadFailed: '{label} konnte nicht geladen werden.',
manifestHint: 'Dieser Tab wird aus manifest.json zusammengesetzt – bauen Sie den Baum oder stellen Sie den gebauten Baum bereit, um ihn zu laden.',
@ -70,8 +70,8 @@ export const STRINGS = {
'tab.upg-100': 'Cartazes da lista UPG 100', 'sub.upg-100': 'minha lista de não alcançados + cartazes',
'tab.country-posters': 'Cartazes por país', 'sub.country-posters': 'um cartaz para cada país',
'tab.poster-design': 'Design de cartazes', 'sub.poster-design': 'todos os estilos e tamanhos de cartaz',
'tab.wall-of-people-groups': 'Mural dos povos', 'tab.all-people-groups': 'Todos os povos',
'tab.upg-neighbor': 'Vizinho UPG', 'tab.bible-posters': 'Cartazes bíblicos',
'tab.wall-of-people-groups': 'Mural dos povos',
'tab.bible-posters': 'Cartazes bíblicos',
experimentalSub: 'mapa experimental', lab: 'beta',
loading: 'Carregando {label}…', loadFailed: 'Não foi possível carregar {label}.',
manifestHint: 'Esta aba é composta a partir do manifest.json — compile a árvore ou sirva a árvore compilada para carregá-la.',
@ -83,8 +83,8 @@ export const STRINGS = {
'tab.upg-100': 'Poster della lista UPG 100', 'sub.upg-100': 'la mia lista dei non raggiunti + poster',
'tab.country-posters': 'Poster per paese', 'sub.country-posters': 'un poster per ogni paese',
'tab.poster-design': 'Design dei poster', 'sub.poster-design': 'tutti gli stili e i formati dei poster',
'tab.wall-of-people-groups': 'Muro dei popoli', 'tab.all-people-groups': 'Tutti i popoli',
'tab.upg-neighbor': 'Vicino UPG', 'tab.bible-posters': 'Poster biblici',
'tab.wall-of-people-groups': 'Muro dei popoli',
'tab.bible-posters': 'Poster biblici',
experimentalSub: 'mappa sperimentale', lab: 'beta',
loading: 'Caricamento di {label}…', loadFailed: 'Impossibile caricare {label}.',
manifestHint: "Questa scheda è composta da manifest.json: compila l'albero o pubblica l'albero compilato per caricarla.",
@ -96,8 +96,8 @@ export const STRINGS = {
'tab.upg-100': 'Afișe din lista UPG 100', 'sub.upg-100': 'lista mea de neatinși + afișe',
'tab.country-posters': 'Afișe pe țări', 'sub.country-posters': 'un afiș pentru fiecare țară',
'tab.poster-design': 'Design de afișe', 'sub.poster-design': 'toate stilurile și dimensiunile de afiș',
'tab.wall-of-people-groups': 'Zidul popoarelor', 'tab.all-people-groups': 'Toate popoarele',
'tab.upg-neighbor': 'Vecin UPG', 'tab.bible-posters': 'Afișe biblice',
'tab.wall-of-people-groups': 'Zidul popoarelor',
'tab.bible-posters': 'Afișe biblice',
experimentalSub: 'hartă experimentală', lab: 'beta',
loading: 'Se încarcă {label}…', loadFailed: '{label} nu a putut fi încărcat.',
manifestHint: 'Această filă este compusă din manifest.json — construiți arborele sau serviți arborele construit pentru a o încărca.',
@ -109,8 +109,8 @@ export const STRINGS = {
'tab.upg-100': 'Плакаты списка UPG 100', 'sub.upg-100': 'мой список недостигнутых + плакаты',
'tab.country-posters': 'Плакаты по странам', 'sub.country-posters': 'плакат для каждой страны',
'tab.poster-design': 'Дизайн плакатов', 'sub.poster-design': 'все стили и размеры плакатов',
'tab.wall-of-people-groups': 'Стена народов', 'tab.all-people-groups': 'Все народы',
'tab.upg-neighbor': 'Сосед UPG', 'tab.bible-posters': 'Библейские плакаты',
'tab.wall-of-people-groups': 'Стена народов',
'tab.bible-posters': 'Библейские плакаты',
experimentalSub: 'экспериментальная карта', lab: 'бета',
loading: 'Загрузка: {label}…', loadFailed: 'Не удалось загрузить: {label}.',
manifestHint: 'Эта вкладка собирается из manifest.json — соберите дерево или разместите собранное дерево, чтобы загрузить её.',
@ -122,8 +122,8 @@ export const STRINGS = {
'tab.upg-100': 'ملصقات قائمة UPG 100', 'sub.upg-100': 'قائمتي لغير المُبشَّرين + ملصقات',
'tab.country-posters': 'ملصقات الدول', 'sub.country-posters': 'ملصق لكل دولة',
'tab.poster-design': 'تصميم الملصقات', 'sub.poster-design': 'كل أنماط الملصقات وأحجامها',
'tab.wall-of-people-groups': 'جدار الشعوب', 'tab.all-people-groups': 'جميع الشعوب',
'tab.upg-neighbor': 'جار UPG', 'tab.bible-posters': 'ملصقات الكتاب المقدس',
'tab.wall-of-people-groups': 'جدار الشعوب',
'tab.bible-posters': 'ملصقات الكتاب المقدس',
experimentalSub: 'خريطة تجريبية', lab: 'تجريبي',
loading: 'جارٍ تحميل {label}…', loadFailed: 'تعذّر تحميل {label}.',
manifestHint: 'تُبنى علامة التبويب هذه من manifest.json — ابنِ الشجرة أو قدّم الشجرة المبنية لتحميلها.',
@ -135,8 +135,8 @@ export const STRINGS = {
'tab.upg-100': 'UPG 100 सूची पोस्टर', 'sub.upg-100': 'मेरी अप्राप्त सूची + पोस्टर',
'tab.country-posters': 'देश पोस्टर', 'sub.country-posters': 'हर देश के लिए एक पोस्टर',
'tab.poster-design': 'पोस्टर डिज़ाइन', 'sub.poster-design': 'हर पोस्टर शैली और आकार',
'tab.wall-of-people-groups': 'जनसमूहों की दीवार', 'tab.all-people-groups': 'सभी जनसमूह',
'tab.upg-neighbor': 'UPG पड़ोसी', 'tab.bible-posters': 'बाइबल पोस्टर',
'tab.wall-of-people-groups': 'जनसमूहों की दीवार',
'tab.bible-posters': 'बाइबल पोस्टर',
experimentalSub: 'प्रायोगिक मानचित्र', lab: 'बीटा',
loading: '{label} लोड हो रहा है…', loadFailed: '{label} लोड नहीं हो सका।',
manifestHint: 'यह टैब manifest.json से बनता है — इसे लोड करने के लिए ट्री बनाएँ या बना हुआ ट्री सर्व करें।',
@ -148,8 +148,8 @@ export const STRINGS = {
'tab.upg-100': 'UPG 100 名单海报', 'sub.upg-100': '我的未得之民名单 + 海报',
'tab.country-posters': '国家海报', 'sub.country-posters': '每个国家一张海报',
'tab.poster-design': '海报设计', 'sub.poster-design': '所有海报样式与尺寸',
'tab.wall-of-people-groups': '民族之墙', 'tab.all-people-groups': '所有民族',
'tab.upg-neighbor': 'UPG 邻居', 'tab.bible-posters': '圣经海报',
'tab.wall-of-people-groups': '民族之墙',
'tab.bible-posters': '圣经海报',
experimentalSub: '实验性地图', lab: '测试版',
loading: '正在加载{label}…', loadFailed: '无法加载{label}。',
manifestHint: '此标签页由 manifest.json 组成——请构建目录树或提供已构建的目录树以加载它。',

View file

@ -10,9 +10,10 @@
* appears, whatever the manifest holds.
*
* ★ A ROW WHOSE BUNDLE IS NOT IN THE TREE IS PRUNED, not painted dead. That is how a row can be listed
* before its plugin lands: Poster Design and Bible Posters are declared with PROVISIONAL ids and stay
* invisible until a bundle of that id exists. Their owners confirm the ids when they start; change the
* `bundle`/`profile` here to match, never the row `id` (the id is the stable key for ?tab= links).
* before its plugin lands: Bible Posters is declared with PROVISIONAL ids and stays invisible until a
* bundle of that id exists. Its owner confirms the ids; change the `bundle`/`profile` here to match,
* never the row `id` (the id is the stable key for ?tab= links). A provisional row's `composes` entry
* lands in the same push as its bundle (check:posterverse-shelf arm D).
*
* ★ MOUNT THE COMPOSITE, NOT A LEAF. The profile named here is the one that mounts. my-upg-100-list and
* country-posters each ship a leaf map, a leaf wall and the capability-4 composite of the two; the
@ -29,11 +30,8 @@ export const SECTIONS = [
export const TABS = [
{ id: 'upg-100', section: 'posters', icon: 'list', bundle: 'my-upg-100-list', profile: 'upg-100-mfe' },
{ id: 'country-posters', section: 'posters', icon: 'map', bundle: 'country-posters', profile: 'country-posters-mfe' },
// Provisional ids: no poster-design bundle exists yet, so this row is pruned until one does.
{ id: 'poster-design', section: 'posters', icon: 'poster', bundle: 'poster-design', profile: 'poster-design-mfe', provisional: true },
{ id: 'poster-design', section: 'posters', icon: 'poster', bundle: 'poster-design', profile: 'poster-design-mfe' },
{ id: 'wall-of-people-groups', section: 'experimental', icon: 'flask', bundle: 'wall-of-people-groups', profile: 'wall-of-people-groups' },
{ id: 'all-people-groups', section: 'experimental', icon: 'flask', bundle: 'all-people-groups', profile: 'all-people-groups' },
{ id: 'upg-neighbor', section: 'experimental', icon: 'flask', bundle: 'upg-neighbor', profile: 'upg-neighbor' },
// Provisional ids: no bible-posters bundle exists yet, so this row is pruned until one does.
{ id: 'bible-posters', section: 'experimental', icon: 'flask', bundle: 'bible-posters', profile: 'bible-posters', provisional: true },
]
@ -47,7 +45,7 @@ export const NEVER = new Set(['key-cities', 'posterverse', 'doxa-maps-spa', 'plu
// (browsers keep ~8-16 live contexts and silently kill the oldest). Taken from the Research Gallery's
// measured HEAVY_ELEMENTS for the tags this rail shows; any profile the manifest marks `isMap` is
// treated as heavy too, so a new map row is bounded without an edit here.
const HEAVY_TAGS = new Set(['my-upg-100-list-map', 'country-posters-map', 'all-people-groups'])
const HEAVY_TAGS = new Set(['my-upg-100-list-map', 'country-posters-map'])
/**
* Complete each row from the manifest: the bundle's scriptUrl and the profile's element tag. A row

View file

@ -38,6 +38,11 @@ export const STRIP_PT = 9 // the one-verse strip under each row, on the
// A preset as panels: its sheets stacked top to bottom, each join a blank GLUE band as deep as the border.
const panelsOf = (P) => planPanels({ sheetW: P.sheetW, sheetH: P.sheetH, count: P.sheets, direction: 'vertical', joinIn: P.border, borderIn: P.border, joinFill: 'glue' })
// The poster's colours, shared with the screen canvas (wallCanvas.js) so the wall shows what prints: the UPG 100
// List template's paper, ink and purple, with a warm grey under a photo that has not loaded.
export const PAPER = '#f7f4ec', INK = '#1c1c17', NAME_INK = '#2A2721', ACCENT = '#6B4A78', PHOTO_BG = '#e9e3d4'
export const VERSE_INK = '#3a362c', REF_INK = '#8a8470'
const CARD_ASPECT = 0.8 // card width / height: a portrait card (name, photo, verse)
const PT = 1 / 72 // one point in inches
@ -51,14 +56,16 @@ export function planPoster(presetId, n) {
const W = P.sheetW - 2 * b
// Top of sheet 1: the title + DOXA line, then the prayer-themes table. Bottom of the last sheet: the
// priestly blessing, the journey from Abraham to the church and the closing line. All scale with the width.
const headH = W * 0.075, tableH = W * 0.17
const bannerH = headH + tableH, footerH = footerHeight(W)
// gapH: air between the chart and the first row of cards, and between the last row and the footer (the grid
// used to butt against the chart's border).
const headH = W * 0.075, tableH = W * 0.17, gapH = W * 0.012
const bannerH = headH + tableH + gapH, footerH = footerHeight(W)
// Each sheet's content band, in the sheet's own coordinates: y0..y1. The panels give each sheet's content box:
// sheet 1 starts below its outer border, the others below their glue band; only the last has a bottom border
// (the others' bottom edge is trimmed flat and glued over the next sheet's band).
const panels = panelsOf(P)
const bands = panels.sheets.map(({ s, last, content: { y0, y1 } }) => {
const top = s === 0 ? bannerH : 0, bottom = last ? footerH : 0
const top = s === 0 ? bannerH : 0, bottom = last ? footerH + gapH : 0
return { s, y0: y0 + top, y1: y1 - bottom, bannerY: s === 0 ? y0 : null, footerY: last ? y1 - footerH : null }
})
// The largest card (fewest columns) whose grid holds n when rows are fitted to each band; `strip` is the
@ -76,12 +83,16 @@ export function planPoster(presetId, n) {
const stripLayout = versePtOf(grid.cw) < MIN_READABLE_PT
if (stripLayout) grid = fit((STRIP_PT * 1.35 + 4) * PT) // one line at STRIP_PT, plus air
const { cols, cw, ch, rowsPer, strip } = grid
// Place cards band by band, row by row; in the strip layout each row's strip sits under it.
// Place cards band by band, row by row; in the strip layout each row's strip sits under it. The rows a band
// actually uses are centred in it, so its spare height is shared equally above and below the grid (it used
// to pool as one wide gap above the footer).
const cards = [], strips = []
let i = 0, row = 0
for (let s = 0; s < bands.length && i < n; s++) {
const used = Math.min(rowsPer[s], Math.ceil((n - i) / cols))
const top = bands[s].y0 + Math.max(0, (bands[s].y1 - bands[s].y0) - used * (ch + strip)) / 2
for (let r = 0; r < rowsPer[s] && i < n; r++, row++) {
const y = bands[s].y0 + r * (ch + strip)
const y = top + r * (ch + strip)
for (let c = 0; c < cols && i < n; c++, i++) cards.push({ i, s, x: b + c * cw, y, w: cw, h: ch })
if (strip) strips.push({ row, s, x: b, y: y + ch, w: W, h: strip })
}
@ -90,7 +101,7 @@ export function planPoster(presetId, n) {
return {
preset: P, presetId: PRESETS[presetId] ? presetId : DEFAULT_PRESET, n, cols, rowsPer, cw, ch, bands, cards,
layout: stripLayout ? 'strip' : 'cards', strips,
bannerH, footerH, headH, tableH, contentW: W,
bannerH, footerH, headH, tableH, gapH, contentW: W,
posterW: panels.finished.w, posterH: panels.finished.h, // after gluing: 60 × 96 for 5x8ft
text: { versePt: +versePt.toFixed(1), namePt: +(versePt * 1.25).toFixed(1), readable: !stripLayout, stripPt: STRIP_PT },
}
@ -102,18 +113,21 @@ export function planPoster(presetId, n) {
// word breaks; the footer is never shallower than a fifth of the width. The same font sizes as the CSS.
// The journey's passages are the long text: never below 6 pt (2 × 3 ft), ~12 pt on the 5 × 8 ft poster, which
// keeps the 5 × 8's verse under every card at 6 pt (a larger footer would shrink the cards below it).
// ONE inner padding for every block (header, chart cells, footer), so all the text shares the same left and
// right edges across the full content width.
const padOf = (W) => W * 0.008
export const FOOTER_FONTS = (W) => ({ small: W * 0.0065, line: W * 0.012, ref: W * 0.0075, name: W * 0.0062, station: Math.max(6 / 72, W * 0.0034), close: W * 0.0085 })
function footerHeight(W) {
const F = FOOTER_FONTS(W), gap = W * 0.006
const lines = (text, width, fs) => Math.max(1, Math.ceil((String(text).length * 0.5 * fs * 1.12) / width))
const B = POSTER.blessing
const blessH = [...B.intro, ...B.outro].reduce((a, t) => a + lines(t, W * 0.8, F.small) * F.small * 1.35, 0)
const blessH = [...B.intro, ...B.outro].reduce((a, t) => a + lines(t, W - 2 * padOf(W), F.small) * F.small * 1.35, 0)
+ B.lines.reduce((a, t) => a + lines(t, W, F.line) * F.line * 1.25, 0) + F.ref * 1.6
const colW = (W - 6 * gap) / 7 - W * 0.008
const stationH = Math.max(...POSTER.journey.map((v) => lines(v.text, colW, F.station) * F.station * 1.3)) + F.name * 1.6 + F.ref * 1.6 + W * 0.006
// the closing line's English sizes its row (its translations run within a few words of it); CLOSING_EN is
// declared below, which is fine: this runs when a poster is planned, long after the module has loaded
const closeH = lines(CLOSING_EN, W * 0.8, F.close) * F.close * 1.35
const closeH = lines(CLOSING_EN, W - 2 * padOf(W), F.close) * F.close * 1.35
return Math.max(W * 0.2, (blessH + stationH + closeH + W * 0.03) * 1.05)
}
@ -136,17 +150,22 @@ const ICONS = {
* be read is noise); verse and reference are only printed on sizes whose verses are readable;
* · a long name shrinks to fit its line (bold ≈ 0.72 em a character: measured wide on purpose, rega found
* 0.68 left "Kummara, Kumbara" 2 px over), down to MIN_READABLE_PT (or the preset's own name size if that
* is smaller); still too long, it wraps, at the size that fits two lines, or three at the floor.
* is smaller); still too long, it wraps, at the size that fits two lines, or more (up to four) at the floor.
*/
export function cardText(plan, name) {
const vPt = plan.text.versePt, nPt = plan.text.namePt
const EM = 0.72, inner = plan.cw * 0.93 / PT, len = Math.max(1, String(name).length)
const floor = Math.min(nPt, MIN_READABLE_PT), want = inner / (len * EM)
const wrap = want < floor
// Wrapped: the size that fits two lines (not below the floor), and as many lines (≤ 3) as that size needs.
const pt = wrap ? Math.max(floor, Math.min(nPt, (2 * inner) / (len * EM))) : Math.min(nPt, want)
// (× 1.2: a line breaks at a word, so it is rarely full; rega measured a 3-line name the plain sum called 2)
const lines = wrap ? Math.min(3, Math.ceil((len * EM * pt * 1.2) / inner)) : 1
// Wrapped: the size that fits two lines (not below the floor), and as many lines (≤ 4) as that size needs.
// A line breaks only between words, so the longest word must fit a line too ("Liechtensteiners" did not, and
// "Deaf Liechtensteiners" printed its third line under the clamp).
const words = String(name).split(/\s+/).filter(Boolean), longest = Math.max(1, ...words.map((w) => w.length))
const pt = wrap ? Math.max(floor, Math.min(nPt, (2 * inner) / (len * EM), inner / (longest * EM))) : Math.min(nPt, want)
// Lines counted the way the browser breaks them: whole words, greedily (a word longer than a line is cut), with
// 15% slack on the character width (rega measured bold names wider than the plain sum). Up to 4 lines: a long
// name at the floor size ("Northern Conchucos Ancash Quechua") needs 4, and the photo gives up the height.
const lines = wrap ? Math.min(4, wrapLines(words, inner / (EM * pt * 1.15))) : 1
return {
readable: plan.text.readable, versePt: vPt, refPt: +Math.max(MIN_READABLE_PT, vPt * 0.8).toFixed(1),
name: { pt: Math.floor(pt * 10) / 10, wrap, lines },
@ -154,6 +173,19 @@ export function cardText(plan, name) {
}
}
// How many lines a list of words takes at perLine characters a line, broken at spaces (a longer word is cut).
function wrapLines(words, perLine) {
let lines = 1, cur = 0
for (const w of words) {
const need = cur ? cur + 1 + w.length : w.length
if (need <= perLine) { cur = need; continue }
if (cur) lines++
cur = w.length
while (cur > perLine) { lines++; cur -= Math.floor(perLine) || 1 }
}
return lines
}
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]))
/**
@ -201,15 +233,22 @@ function posterParts(groups, presetId, texts, withCards = true) {
const W = plan.contentW, FF = FOOTER_FONTS(W) // the footer's sizes: the same numbers footerHeight() plans with
const vPt = plan.text.versePt, rPt = cardText(plan, '').refPt
// The rotated sidebar labels of the chart are sized from their text: the longest word must fit the row's
// height (a CJK label stands upright, one em a character; other scripts ≈ 0.72 em a capital with the
// letter spacing), and the wrapped lines must fit the sidebar's width.
// height (a CJK label stands upright, one em a character; other scripts ≈ 0.84 em a capital: 0.72 for the
// letter, 0.06 of letter spacing, and slack, since "TESTAMENT" at 0.72 overran its row), and the wrapped lines
// must fit the sidebar's width.
const railH = plan.tableH * 0.33 - W * 0.006, railW = W * 0.06 - W * 0.004
const railFs = (text) => {
const words = String(text).split(/\s+/), em = upright ? 1.02 : 0.72
const words = String(text).split(/\s+/), em = upright ? 1.02 : 0.84
const longest = Math.max(...words.map((w) => w.length)) * em
const byH = railH / longest, byW = railW / (Math.min(words.length, 3) * 1.2)
return Math.min(W * 0.0105, byH, byW)
}
// The chart's verse cells: one size for all six, the largest (up to W × 0.0078) at which the longest cell
// (verse at line-height 1.3, then its reference) fits its row. Characters ≈ 0.5 em with 15% slack for breaks.
const cellW = (W - W * 0.06) / 3 - 2 * W * 0.008, cellH = plan.tableH * 0.33 - 2 * W * 0.006 - W * 0.002 - W * 0.0062 * 1.3
const cellFits = (fs) => THEMES.every((t) => [t.ot, t.nt].every((v) => Math.ceil((String(v.text).length * 0.5 * fs * 1.15) / cellW) * fs * 1.3 <= cellH))
let tvFs = W * 0.0078
while (tvFs > W * 0.005 && !cellFits(tvFs)) tvFs *= 0.97
const railLabel = (text) => `<span style="font-size:${f(railFs(text))}">${esc(text)}</span>`
const card = (c) => {
const g = groups[c.i], v = blessingFor(c.i)
@ -256,46 +295,55 @@ function posterParts(groups, presetId, texts, withCards = true) {
return `<div class="vstrip" dir="auto" style="left:${f(t.x)};top:${f(t.y)};width:${f(t.w)};height:${f(t.h)}">${esc(v.text)}<span class="vref">${esc(v.ref)}</span></div>` }).join('')
return { cards: withCards ? plan.cards.filter((c) => c.s === s).map(card).join('') : '', chrome: `${marks.before}${head}${strips}${foot}${marks.after}` }
})
// THE HOUSE POSTER STYLE, from the UPG 100 List template (library/packet/poster-render.js, the approved v5):
// warm paper, rules instead of boxes, Georgia for titles and scripture, the sans for labels and names,
// tracked capitals for the small labels, and the purple ink for references and emphasis.
const PAD = padOf(W)
const SANS = "Lato,-apple-system,'Segoe UI',system-ui,Arial,sans-serif"
const css = `@page{size:${P.sheetW}in ${P.sheetH}in;margin:0}
*{box-sizing:border-box}html,body{margin:0;padding:0}
body,.poster{font-family:Georgia,'Times New Roman',serif;color:#1c1c1c;-webkit-print-color-adjust:exact;print-color-adjust:exact}
.sheet{position:relative;width:${P.sheetW}in;height:${P.sheetH}in;overflow:hidden;page-break-after:always;break-after:page}
body,.poster{font-family:Georgia,'Times New Roman',serif;color:${INK};-webkit-print-color-adjust:exact;print-color-adjust:exact}
.sheet{position:relative;width:${P.sheetW}in;height:${P.sheetH}in;overflow:hidden;page-break-after:always;break-after:page;background:${PAPER}}
.ban{position:absolute;left:${f(b)};width:${f(W)};text-align:center}
.hd{display:flex;flex-direction:column;justify-content:center;align-items:center}
.hd h1{margin:0;font-size:${f(plan.headH * 0.44)};line-height:1.05;font-weight:700}
.hd .subt{margin:${f(plan.headH * 0.06)} auto 0;max-width:78%;font:${f(plan.headH * 0.085)}/1.35 -apple-system,'Segoe UI',Arial,sans-serif;color:#333}
.themes{display:grid;grid-template-columns:${f(W * 0.06)} repeat(3,1fr);grid-template-rows:34% 33% 33%;height:100%;border:.03in solid #1c1c1c;border-radius:${f(W * 0.006)};overflow:hidden}
.themes > div{border:.015in solid #cfcfcf;padding:${f(W * 0.006)} ${f(W * 0.008)};overflow:hidden}
.themes > .rl{display:flex;align-items:center;justify-content:center;background:#1c1c1c;color:#fff;padding:${f(W * 0.002)}}
.rl span{writing-mode:vertical-rl;${upright ? 'text-orientation:upright;' : 'transform:rotate(180deg);'}font-weight:700;font-family:-apple-system,'Segoe UI',Arial,sans-serif;line-height:1.15;letter-spacing:.06em;text-transform:uppercase;text-align:center}
.themes > .th{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:${f(W * 0.003)};background:#faf8f2;padding-top:${f(W * 0.003)};padding-bottom:${f(W * 0.003)}}
.th .ic{width:${f(W * 0.028)};height:${f(W * 0.028)};color:#6b4f1d;flex:none}.th .ic svg{width:100%;height:100%}
.th .tn{font:700 ${f(W * 0.012)}/1.1 Georgia,serif;letter-spacing:.04em;text-transform:uppercase}
.hd{padding:0 ${f(PAD)}}
.hd{display:flex;flex-direction:column;justify-content:center;align-items:center;border-bottom:.015in solid #C9BFA4}
.hd h1{margin:0;font-size:${f(plan.headH * 0.44)};line-height:1.15;font-weight:700;letter-spacing:-.02em;color:#1c1c17}
.hd .subt{margin:${f(plan.headH * 0.06)} 0 0;font:${f(plan.headH * 0.085)}/1.35 ${SANS};color:#7E7562;letter-spacing:.01em}
.themes{display:grid;grid-template-columns:${f(W * 0.06)} repeat(3,1fr);grid-template-rows:34% 33% 33%;height:100%;border-bottom:.02in solid #C9BFA4;overflow:hidden}
.themes > div{padding:${f(W * 0.006)} ${f(PAD)};overflow:hidden;border-inline-start:.01in solid #D6CCB4}
.themes > .rl{display:flex;align-items:center;justify-content:center;color:#8a8470;padding:${f(W * 0.002)};border-inline-start:0}
.themes > .td,.themes > .rl:not(.rl-h){border-top:.01in solid #D6CCB4}
.rl span{writing-mode:vertical-rl;${upright ? 'text-orientation:upright;' : 'transform:rotate(180deg);'}font-weight:700;font-family:${SANS};line-height:1.15;letter-spacing:.06em;text-transform:uppercase;text-align:center}
.themes > .th{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:${f(W * 0.003)};padding-top:${f(W * 0.003)};padding-bottom:${f(W * 0.003)}}
.th .ic{width:${f(W * 0.028)};height:${f(W * 0.028)};color:${ACCENT};flex:none}.th .ic svg{width:100%;height:100%}
.th .tn{font:700 ${f(W * 0.012)}/1.1 Georgia,serif;letter-spacing:.02em;color:${ACCENT}}
.td{display:flex;flex-direction:column;justify-content:center;text-align:center}
.td .tv{margin:0;font-size:${f(W * 0.0078)};line-height:1.3;font-style:italic}
.td .tr{margin:${f(W * 0.002)} 0 0;font-size:${f(W * 0.0062)};color:#666}
.ftr{position:absolute;left:${f(b)};width:${f(W)};display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;border-top:.03in solid #1c1c1c}
.bless p{margin:0;font-size:${f(FF.line)};line-height:1.25;font-weight:600}
.bless p.bi{max-width:80%;margin:${f(W * 0.001)} auto;font-size:${f(FF.small)};line-height:1.35;font-weight:400;font-style:italic;color:#444}
.bless .br{font-size:${f(FF.ref)};font-weight:400;color:#666;margin-top:${f(W * 0.002)}}
.jny{display:grid;grid-template-columns:repeat(7,1fr);gap:${f(W * 0.006)};margin-top:${f(W * 0.01)};width:100%;align-items:stretch}
.st{display:flex;flex-direction:column;justify-content:center;padding:${f(W * 0.003)} ${f(W * 0.004)};border-top:.02in solid #c9b58a}
.st .sn{margin:0 0 ${f(W * 0.002)};font:700 ${f(FF.name)} Georgia,serif;letter-spacing:.05em;text-transform:uppercase;color:#6b4f1d}
.st .sv{margin:0;font-size:${f(FF.station)};font-style:italic;line-height:1.3}
.st .sr{margin:${f(W * 0.002)} 0 0;font-size:${f(Math.max(6 / 72, W * 0.0048))};color:#666}
.st.ctr{background:#faf8f2;border:.03in solid #1c1c1c}.st.ctr .sv{font-weight:600}.st.ctr .sn{color:#1c1c1c}
.close{margin:${f(W * 0.008)} auto 0;max-width:80%;font:700 ${f(FF.close)}/1.35 -apple-system,'Segoe UI',Arial,sans-serif}
.td .tv{margin:0;font-size:${f(tvFs)};line-height:1.3;font-style:italic;color:#3a362c}
.td .tr{margin:${f(W * 0.002)} 0 0;font:700 ${f(W * 0.0055)} ${SANS};letter-spacing:.12em;text-transform:uppercase;color:${ACCENT}}
.ftr{position:absolute;left:${f(b)};width:${f(W)};padding:0 ${f(PAD)};display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;border-top:.02in solid #C9BFA4}
.bless p{margin:0;font-size:${f(FF.line)};line-height:1.25;font-weight:700;font-style:italic;color:#2A2721}
.bless p.bi{margin:${f(W * 0.001)} 0;font-size:${f(FF.small)};line-height:1.35;font-weight:400;font-style:italic;color:#6a6450}
.bless .br{margin-top:${f(W * 0.002)};font:700 ${f(FF.ref)} ${SANS};letter-spacing:.14em;text-transform:uppercase;color:${ACCENT};font-style:normal}
/* Rules run the full content width, like the chart's; the padding belongs to the text. */
.jny{display:grid;grid-template-columns:repeat(7,1fr);gap:${f(W * 0.006)};margin:${f(W * 0.01)} ${f(-PAD)} 0;width:${f(W)};align-items:stretch;border-top:.01in solid #D6CCB4;padding-top:${f(W * 0.004)}}
.st{display:flex;flex-direction:column;justify-content:flex-start;padding:${f(W * 0.003)} ${f(W * 0.004)}}
.st + .st{border-inline-start:.01in solid #D6CCB4}
.st .sn{margin:0 0 ${f(W * 0.002)};font:700 ${f(FF.name)} ${SANS};letter-spacing:.08em;text-transform:uppercase;color:#8a8470}
.st .sv{margin:0;font-size:${f(FF.station)};font-style:italic;line-height:1.3;color:#4a4636}
.st .sr{margin:${f(W * 0.002)} 0 0;font:700 ${f(Math.max(6 / 72, W * 0.0048))} ${SANS};letter-spacing:.1em;text-transform:uppercase;color:${ACCENT}}
.st.ctr{background:#F2EFE6;border:.02in solid #cdbf9e}.st.ctr + .st{border-inline-start:0}.st.ctr .sv{font-weight:600;color:#2A2721}.st.ctr .sn{color:${ACCENT}}
.close{margin:${f(W * 0.008)} 0 0;font:italic 700 ${f(FF.close)}/1.35 Georgia,'Times New Roman',serif;color:#2A2721}
.vstrip{position:absolute;display:flex;align-items:center;justify-content:center;gap:.6em;font-size:${STRIP_PT}pt;font-style:italic;
color:#2d2d2d;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-top:.01in solid #e6e6e6}
.vstrip .vref{font-style:normal;color:#777;font-size:.85em}
color:#3a362c;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-top:.01in solid #D6CCB4}
.vstrip .vref{font:700 .8em ${SANS};letter-spacing:.12em;text-transform:uppercase;color:${ACCENT}}
.card{position:absolute;padding:${f(plan.cw * 0.035)};display:flex;flex-direction:column;align-items:center;text-align:center;overflow:hidden}
.card .nm{font-weight:700;line-height:1.1;font-family:-apple-system,'Segoe UI',Arial,sans-serif;width:100%;white-space:nowrap;overflow:hidden;flex:none}
.card .nm{font-weight:700;line-height:1.1;font-family:${SANS};color:${NAME_INK};width:100%;white-space:nowrap;overflow:hidden;flex:none}
.card .nm.w2{white-space:normal;overflow-wrap:anywhere;hyphens:auto;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
/* The photo takes whatever height the text leaves, so a card's content never outgrows the card. */
.card img,.card .noimg{width:100%;flex:1 1 0;min-height:0;object-fit:cover;margin:${f(plan.cw * 0.02)} 0;background:#eee}
.card .vs{font-size:${vPt}pt;line-height:1.15;font-style:italic}
.card img,.card .noimg{width:100%;flex:1 1 0;min-height:0;object-fit:cover;margin:${f(plan.cw * 0.02)} 0;background:${PHOTO_BG}}
.card .vs{font-size:${vPt}pt;line-height:1.15;font-style:italic;color:${VERSE_INK}}
.card .vs,.card .rf{flex:none}
.card .rf{font-size:${rPt.toFixed(1)}pt;color:#777;margin-top:1pt}
.card .rf{font-size:${rPt.toFixed(1)}pt;font-family:${SANS};color:${REF_INK};margin-top:1pt}
${panelCss(panels)}`
return { plan, css, pages, banner }
}

View file

@ -17,7 +17,10 @@
import { wallD3 } from './wallD3.js'
import { wallGrid } from './wallLod.js'
import { blessingFor, wrapText } from './blessings.js'
import { cardText } from './blessPoster.js'
import { cardText, PAPER, NAME_INK, VERSE_INK, REF_INK, PHOTO_BG } from './blessPoster.js'
// The print's sans (blessPoster SANS), for the names and references drawn on the canvas.
const SANS = 'Lato, -apple-system, "Segoe UI", system-ui, Arial, sans-serif'
// 'rtl' when a string's first strong character is Hebrew/Arabic-script, else 'ltr' (canvas has no dir="auto").
const RTL_FIRST = /^[^A-Za-z\u00C0-\u024F\u0370-\u052F\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]*[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]/
@ -121,9 +124,10 @@ export class WallCanvas {
this.host.appendChild(this.ov)
this.ovRoot = this.ov.attachShadow ? this.ov.attachShadow({ mode: 'open' }) : this.ov
}
// The print CSS (its @page rule is ignored on screen), scoped by the shadow root. The overlay starts at
// The print CSS (its @page rule is ignored on screen), scoped by the shadow root. The overlay's sheets are
// transparent: the canvas under them paints the paper and the cards (the print's paper colour would hide them). The overlay starts at
// scale(0) and takes the zoom's transform on the next frame, so it never flashes at full print size.
this.ovRoot.innerHTML = `<style>${sp.css}\n.ovw{position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none}.ovw a{pointer-events:auto}.ovw .sheet{content-visibility:auto}</style>`
this.ovRoot.innerHTML = `<style>${sp.css}\n.ovw{position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none}.ovw a{pointer-events:auto}.ovw .sheet{content-visibility:auto;background:none}</style>`
+ `<div class="ovw poster" style="width:${sp.worldW}px;height:${sp.worldH}px;transform:scale(0)">${sp.chromeHtml}</div>`
this.ovw = this.ovRoot.querySelector('.ovw')
this.showPoster(); this.resize()
@ -232,7 +236,7 @@ export class WallCanvas {
ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.fillStyle = '#d4d4d4'; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); ctx.restore()
ctx.fillStyle = 'rgba(0,0,0,.16)'
for (const R of sp.sheets) ctx.fillRect(R.x + 3 / s, R.y + 3 / s, R.w, R.h)
ctx.fillStyle = '#fff'
ctx.fillStyle = PAPER
for (const R of sp.sheets) ctx.fillRect(R.x, R.y, R.w, R.h)
const [x0, y0] = this.toWorld(0, 0), [x1, y1] = this.toWorld(this.cssW, this.cssH)
const cardPx = (sp.cards[0]?.w || 0) * s, want = []
@ -241,7 +245,7 @@ export class WallCanvas {
for (const R of sp.cards) {
if (R.x + R.w < x0 || R.x > x1 || R.y + R.h < y0 || R.y > y1) continue
if (R.i >= this.groups.length) continue
if (cardPx < RECT_PX) { ctx.fillStyle = '#e6e6e6'; ctx.fillRect(R.x + R.w * 0.04, R.y + R.h * 0.04, R.w * 0.92, R.h * 0.92); continue }
if (cardPx < RECT_PX) { ctx.fillStyle = PHOTO_BG; ctx.fillRect(R.x + R.w * 0.04, R.y + R.h * 0.04, R.w * 0.92, R.h * 0.92); continue }
this.drawPrintCard(ctx, R, cardPx, want)
}
if (want.length) this.load(want)
@ -271,7 +275,7 @@ export class WallCanvas {
} else if (!T) {
this.measureBudget--; this.stats.measured++
// Line breaks measured once per card (the fonts are the print's), then cached.
ctx.font = `700 ${nameFs}px -apple-system, "Segoe UI", Arial, sans-serif`
ctx.font = `700 ${nameFs}px ${SANS}`
const nameLines = ct.name.wrap ? this.breakLines(ctx, name, iw, ct.name.lines) : [name]
let verse = null
if (ct.readable) {
@ -293,21 +297,21 @@ export class WallCanvas {
if (ar > box) { sw = sh * box; sx = (pw(im) - sw) / 2 } else { sh = sw / box; sy = (ph(im) - sh) / 2 }
ctx.drawImage(im, sx, sy, sw, sh, x, imgY, iw, imgH) // object-fit: cover
} else {
ctx.fillStyle = '#eee'; ctx.fillRect(x, imgY, iw, imgH)
ctx.fillStyle = PHOTO_BG; ctx.fillRect(x, imgY, iw, imgH)
if (url && cardPx >= IMG_PX) want.push(url)
}
if (cardPx < TEXT_PX || T.estimate) return // no text yet (below text zoom, or waiting its turn to be measured)
ctx.textAlign = 'center'; ctx.textBaseline = 'top'
ctx.font = `700 ${nameFs}px -apple-system, "Segoe UI", Arial, sans-serif`
ctx.fillStyle = '#1c1c1c' // as printed; a linked name shows the pointer cursor instead of link blue
ctx.font = `700 ${nameFs}px ${SANS}`
ctx.fillStyle = NAME_INK // as printed; a linked name shows the pointer cursor instead of link blue
ctx.direction = textDir(name)
T.nameLines.forEach((ln, k) => ctx.fillText(ln, cx, R.y + pad + k * nameFs * ct.nameLine, iw))
if (T.verse) {
let y = imgY + imgH + m
ctx.font = `italic ${vFs}px Georgia, "Times New Roman", serif`; ctx.fillStyle = '#1c1c1c'
ctx.font = `italic ${vFs}px Georgia, "Times New Roman", serif`; ctx.fillStyle = VERSE_INK
ctx.direction = textDir(T.verse.lines[0])
for (const ln of T.verse.lines) { ctx.fillText(ln, cx, y, iw); y += vFs * ct.verseLine }
ctx.font = `${rFs}px Georgia, "Times New Roman", serif`; ctx.fillStyle = '#777'
ctx.font = `${rFs}px ${SANS}`; ctx.fillStyle = REF_INK
ctx.direction = textDir(T.verse.ref)
ctx.fillText(T.verse.ref, cx, y + ct.refGapPt * pt, iw)
}

View file

@ -2,7 +2,7 @@
This folder is GENERATED by `bun run build`; do not edit it by hand. It holds exactly one thing:
- **`dx-maps/`** — the output folder: 26 plugin bundles and 38 pages, self-contained.
- **`dx-maps/`** — the output folder: 27 plugin bundles and 41 pages, self-contained.
Copy this one folder to any web host (or open it from disk) and it works.
- `dx-maps/index.html` — the front page: the Plugin Studio, every plugin in one place.
- `dx-maps/<plugin>/<plugin>.js` — a plugin's bundle (a custom element; load it with a `<script>` tag).

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>all-people-groups · all-people-groups</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -51,7 +51,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -51,7 +51,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -51,7 +51,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>connectivity-map · cities-internet</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>country-map · country-map</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>countries-map · country-posters</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
@ -76,7 +76,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-LHY6hzhphKMTlAzfb/Ty2uCu+O87CtDgqYayok4PZINsY/h42Fc4jwpVZ2jcEe2A" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-ZX5sAkdVfB1b7Ysy+153Vif0MJpn+FkYEnFe+qKMCpaJr6JSb4yRwmtmik8qlKpc" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
@ -67,7 +67,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-LHY6hzhphKMTlAzfb/Ty2uCu+O87CtDgqYayok4PZINsY/h42Fc4jwpVZ2jcEe2A" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-ZX5sAkdVfB1b7Ysy+153Vif0MJpn+FkYEnFe+qKMCpaJr6JSb4yRwmtmik8qlKpc" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>country-posters-mfe · country-posters</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -60,7 +60,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
@ -81,7 +81,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-LHY6hzhphKMTlAzfb/Ty2uCu+O87CtDgqYayok4PZINsY/h42Fc4jwpVZ2jcEe2A" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-ZX5sAkdVfB1b7Ysy+153Vif0MJpn+FkYEnFe+qKMCpaJr6JSb4yRwmtmik8qlKpc" 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

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>default · dashboard-plus-grid</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -60,7 +60,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>dashboard-plus-grid · dashboard-plus-grid</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -60,7 +60,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>demo-map · demo-map</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Demo map — MapLibre (token-free) · demo-map</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -51,7 +51,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

File diff suppressed because one or more lines are too long

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
<script src="../../doxa-maps-spa.js" integrity="sha384-FVAW1+l/o0xARsf8Tv40pKDxqGmoOHjgoUTI5E4a9WfTaZypsvNfzD/8/HH2sVu+" crossorigin="anonymous"></script>
<script src="../../doxa-maps-spa.js" integrity="sha384-I6e3h2TFC+urJVeJBUpvnQmmG1oT8SE0qtUNZKGnzhUDCx64/S8NlFhhIjl6oO+t" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
<script src="../doxa-maps-spa.js" integrity="sha384-FVAW1+l/o0xARsf8Tv40pKDxqGmoOHjgoUTI5E4a9WfTaZypsvNfzD/8/HH2sVu+" crossorigin="anonymous"></script>
<script src="../doxa-maps-spa.js" integrity="sha384-I6e3h2TFC+urJVeJBUpvnQmmG1oT8SE0qtUNZKGnzhUDCx64/S8NlFhhIjl6oO+t" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -2,8 +2,8 @@
"schema": "doxa-maps-manifest@1",
"generatedFrom": "plugin-designs/*/*.vue",
"embedBase": ".",
"bundleCount": 26,
"profileCount": 38,
"bundleCount": 27,
"profileCount": 41,
"runtime": {
"apiBaseUrl": "https://pray.doxa.life"
},
@ -200,7 +200,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-LHY6hzhphKMTlAzfb/Ty2uCu+O87CtDgqYayok4PZINsY/h42Fc4jwpVZ2jcEe2A",
"integrity": "sha384-ZX5sAkdVfB1b7Ysy+153Vif0MJpn+FkYEnFe+qKMCpaJr6JSb4yRwmtmik8qlKpc",
"elements": [
"country-posters-map"
],
@ -223,8 +223,8 @@
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-LHY6hzhphKMTlAzfb/Ty2uCu+O87CtDgqYayok4PZINsY/h42Fc4jwpVZ2jcEe2A\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-LHY6hzhphKMTlAzfb/Ty2uCu+O87CtDgqYayok4PZINsY/h42Fc4jwpVZ2jcEe2A\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ZX5sAkdVfB1b7Ysy+153Vif0MJpn+FkYEnFe+qKMCpaJr6JSb4yRwmtmik8qlKpc\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ZX5sAkdVfB1b7Ysy+153Vif0MJpn+FkYEnFe+qKMCpaJr6JSb4yRwmtmik8qlKpc\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -240,7 +240,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-LHY6hzhphKMTlAzfb/Ty2uCu+O87CtDgqYayok4PZINsY/h42Fc4jwpVZ2jcEe2A\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"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-ZX5sAkdVfB1b7Ysy+153Vif0MJpn+FkYEnFe+qKMCpaJr6JSb4yRwmtmik8qlKpc\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -256,8 +256,8 @@
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-LHY6hzhphKMTlAzfb/Ty2uCu+O87CtDgqYayok4PZINsY/h42Fc4jwpVZ2jcEe2A\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-LHY6hzhphKMTlAzfb/Ty2uCu+O87CtDgqYayok4PZINsY/h42Fc4jwpVZ2jcEe2A\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ZX5sAkdVfB1b7Ysy+153Vif0MJpn+FkYEnFe+qKMCpaJr6JSb4yRwmtmik8qlKpc\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ZX5sAkdVfB1b7Ysy+153Vif0MJpn+FkYEnFe+qKMCpaJr6JSb4yRwmtmik8qlKpc\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -361,7 +361,7 @@
"bundle": "doxa-maps-spa",
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
"integrity": "sha384-FVAW1+l/o0xARsf8Tv40pKDxqGmoOHjgoUTI5E4a9WfTaZypsvNfzD/8/HH2sVu+",
"integrity": "sha384-I6e3h2TFC+urJVeJBUpvnQmmG1oT8SE0qtUNZKGnzhUDCx64/S8NlFhhIjl6oO+t",
"elements": [
"doxa-maps-spa"
],
@ -371,6 +371,7 @@
"regions-dashboard/regions-mfe",
"my-upg-100-list/upg-100-mfe",
"country-posters/country-posters-mfe",
"poster-design/poster-design-mfe",
"doxa-simple-map/doxa-simple-map",
"cities-internet/connectivity-map",
"gospel-resources/resources-map"
@ -380,7 +381,7 @@
{
"profile": "doxa-maps-spa",
"stage": "experimental",
"label": "Deep Research (vertical-tab app shell: the eight production maps)",
"label": "Deep Research (vertical-tab app shell: the nine production maps)",
"file": "doxa-maps-spa.vue",
"description": "The DX-maps app itself: one element whose vertical-tab rail nests every other",
"element": "doxa-maps-spa",
@ -390,7 +391,7 @@
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"embed": {
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/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-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-FVAW1+l/o0xARsf8Tv40pKDxqGmoOHjgoUTI5E4a9WfTaZypsvNfzD/8/HH2sVu+\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-I6e3h2TFC+urJVeJBUpvnQmmG1oT8SE0qtUNZKGnzhUDCx64/S8NlFhhIjl6oO+t\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
},
"instances": [
{
@ -881,7 +882,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-cJE79obFBKiaPMjchy0migTIZs98CYGbMBZzqIw0LhuGtjn/ak6ItaRx7YNWSqba",
"integrity": "sha384-pDPN4yV8Xrj7wukuzFgnDKFezDsTif+qjCSjmVxrh73CRoh0dx3c/L9XxHcgvkhe",
"elements": [
"my-upg-100-list-map"
],
@ -904,7 +905,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-cJE79obFBKiaPMjchy0migTIZs98CYGbMBZzqIw0LhuGtjn/ak6ItaRx7YNWSqba\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-pDPN4yV8Xrj7wukuzFgnDKFezDsTif+qjCSjmVxrh73CRoh0dx3c/L9XxHcgvkhe\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -920,8 +921,8 @@
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-cJE79obFBKiaPMjchy0migTIZs98CYGbMBZzqIw0LhuGtjn/ak6ItaRx7YNWSqba\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-cJE79obFBKiaPMjchy0migTIZs98CYGbMBZzqIw0LhuGtjn/ak6ItaRx7YNWSqba\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-pDPN4yV8Xrj7wukuzFgnDKFezDsTif+qjCSjmVxrh73CRoh0dx3c/L9XxHcgvkhe\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-pDPN4yV8Xrj7wukuzFgnDKFezDsTif+qjCSjmVxrh73CRoh0dx3c/L9XxHcgvkhe\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -937,8 +938,8 @@
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-cJE79obFBKiaPMjchy0migTIZs98CYGbMBZzqIw0LhuGtjn/ak6ItaRx7YNWSqba\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-cJE79obFBKiaPMjchy0migTIZs98CYGbMBZzqIw0LhuGtjn/ak6ItaRx7YNWSqba\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-pDPN4yV8Xrj7wukuzFgnDKFezDsTif+qjCSjmVxrh73CRoh0dx3c/L9XxHcgvkhe\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-pDPN4yV8Xrj7wukuzFgnDKFezDsTif+qjCSjmVxrh73CRoh0dx3c/L9XxHcgvkhe\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]
@ -1065,20 +1066,83 @@
}
]
},
{
"bundle": "poster-design",
"scriptUrl": "./poster-design/poster-design.js",
"output": "dx-maps/poster-design/poster-design.js",
"integrity": "sha384-GF5Ut8SIc7GswQSaGhl7h4o9gcivEBaOgFqI30pkEr/NZi76JVjnZF4FyzRLU8+U",
"elements": [
"poster-design"
],
"composes": [
"poster-design/poster-styles",
"poster-design/poster-sizes"
],
"stage": "experimental",
"profiles": [
{
"profile": "poster-design-mfe",
"stage": "experimental",
"label": "Poster Design — Styles + Sizes",
"file": "poster-design-mfe.vue",
"description": "Poster Design: every poster template (Styles) and every poster size (Sizes) side by side,",
"element": "poster-design",
"type": "nested-elements-mfe",
"isMap": false,
"iframeUrl": "./poster-design/poster-design-mfe/index.html",
"scriptUrl": "./poster-design/poster-design.js",
"embed": {
"iframe": "<iframe src=\"./poster-design/poster-design-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=\"./poster-design/poster-design.js\" integrity=\"sha384-GF5Ut8SIc7GswQSaGhl7h4o9gcivEBaOgFqI30pkEr/NZi76JVjnZF4FyzRLU8+U\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-design-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
}
},
{
"profile": "poster-sizes",
"stage": "experimental",
"label": "Poster Design — Sizes",
"file": "poster-sizes.vue",
"description": "Every poster size the print picker offers (standard paper up to 5 × 8 ft), each drawn as a",
"element": "poster-design",
"type": "single",
"isMap": false,
"iframeUrl": "./poster-design/poster-sizes/index.html",
"scriptUrl": "./poster-design/poster-design.js",
"embed": {
"iframe": "<iframe src=\"./poster-design/poster-sizes/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=\"./poster-design/poster-design.js\" integrity=\"sha384-GF5Ut8SIc7GswQSaGhl7h4o9gcivEBaOgFqI30pkEr/NZi76JVjnZF4FyzRLU8+U\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-sizes\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
}
},
{
"profile": "poster-styles",
"stage": "experimental",
"label": "Poster Design — Styles",
"file": "poster-styles.vue",
"description": "Every poster template in one grid (map sheets, the prayer packet, prayer cards, the wall",
"element": "poster-design",
"type": "single",
"isMap": false,
"iframeUrl": "./poster-design/poster-styles/index.html",
"scriptUrl": "./poster-design/poster-design.js",
"embed": {
"iframe": "<iframe src=\"./poster-design/poster-styles/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=\"./poster-design/poster-design.js\" integrity=\"sha384-GF5Ut8SIc7GswQSaGhl7h4o9gcivEBaOgFqI30pkEr/NZi76JVjnZF4FyzRLU8+U\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-styles\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
}
}
]
},
{
"bundle": "posterverse",
"scriptUrl": "./posterverse/posterverse.js",
"output": "dx-maps/posterverse/posterverse.js",
"integrity": "sha384-xwStH6d1rIR+LOJ9iBnJXyNFIMTzSQ51YA2+UhqJgloN4EeH6vYubtHY3JM9j7p0",
"integrity": "sha384-cUPtOfPEnuDQSMNh2SpAT6ndNhpj+lXDHzCW+a4jjjGMLlnhjWEilsirlD9vqYZg",
"elements": [
"posterverse-app"
],
"composes": [
"my-upg-100-list/upg-100-mfe",
"country-posters/country-posters-mfe",
"wall-of-people-groups/wall-of-people-groups",
"all-people-groups/all-people-groups",
"upg-neighbor/upg-neighbor"
"poster-design/poster-design-mfe",
"wall-of-people-groups/wall-of-people-groups"
],
"stage": "experimental",
"profiles": [
@ -1095,7 +1159,7 @@
"scriptUrl": "./posterverse/posterverse.js",
"embed": {
"iframe": "<iframe src=\"./posterverse/posterverse/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=\"./posterverse/posterverse.js\" integrity=\"sha384-xwStH6d1rIR+LOJ9iBnJXyNFIMTzSQ51YA2+UhqJgloN4EeH6vYubtHY3JM9j7p0\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<posterverse-app profile-config='{\"profile\":\"posterverse\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></posterverse-app>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./posterverse/posterverse.js\" integrity=\"sha384-cUPtOfPEnuDQSMNh2SpAT6ndNhpj+lXDHzCW+a4jjjGMLlnhjWEilsirlD9vqYZg\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<posterverse-app profile-config='{\"profile\":\"posterverse\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></posterverse-app>"
},
"instances": [
{
@ -1180,7 +1244,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-iIeVdcfWpe2CnYJKFP7/ANLc+QaBWug0eZzk78gX9ymL05/ZNkgiUDE2cu+qHfCM",
"integrity": "sha384-HjKZ8gbrIgybdRdUjN8nBoiA00S3L108xTHEd62y5+0z/hkPHPS39LZLU6XwNsJ0",
"elements": [
"regions-dashboard"
],
@ -1203,8 +1267,8 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-iIeVdcfWpe2CnYJKFP7/ANLc+QaBWug0eZzk78gX9ymL05/ZNkgiUDE2cu+qHfCM\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-iIeVdcfWpe2CnYJKFP7/ANLc+QaBWug0eZzk78gX9ymL05/ZNkgiUDE2cu+qHfCM\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-HjKZ8gbrIgybdRdUjN8nBoiA00S3L108xTHEd62y5+0z/hkPHPS39LZLU6XwNsJ0\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-HjKZ8gbrIgybdRdUjN8nBoiA00S3L108xTHEd62y5+0z/hkPHPS39LZLU6XwNsJ0\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1220,7 +1284,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-iIeVdcfWpe2CnYJKFP7/ANLc+QaBWug0eZzk78gX9ymL05/ZNkgiUDE2cu+qHfCM\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-HjKZ8gbrIgybdRdUjN8nBoiA00S3L108xTHEd62y5+0z/hkPHPS39LZLU6XwNsJ0\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1236,8 +1300,8 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-iIeVdcfWpe2CnYJKFP7/ANLc+QaBWug0eZzk78gX9ymL05/ZNkgiUDE2cu+qHfCM\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-iIeVdcfWpe2CnYJKFP7/ANLc+QaBWug0eZzk78gX9ymL05/ZNkgiUDE2cu+qHfCM\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-HjKZ8gbrIgybdRdUjN8nBoiA00S3L108xTHEd62y5+0z/hkPHPS39LZLU6XwNsJ0\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-HjKZ8gbrIgybdRdUjN8nBoiA00S3L108xTHEd62y5+0z/hkPHPS39LZLU6XwNsJ0\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]
@ -1473,7 +1537,7 @@
"bundle": "wall-of-people-groups",
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"output": "dx-maps/wall-of-people-groups/wall-of-people-groups.js",
"integrity": "sha384-+0FF2Yam1UsHfo2turc9M9IORYEfg/eKrOjl3VLUymNUK7P8/AnL8/p9zlrf/NEh",
"integrity": "sha384-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF",
"elements": [
"wall-of-people-groups"
],
@ -1492,7 +1556,7 @@
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./wall-of-people-groups/wall-of-people-groups/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=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-+0FF2Yam1UsHfo2turc9M9IORYEfg/eKrOjl3VLUymNUK7P8/AnL8/p9zlrf/NEh\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-EQcPfsC0SN3qR+HvILq9491mHiDQSWOfhK0SVrvNSRaZzQ8hqYlZiy5/fmaiJKNF\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
},
"bundle": "wall-of-people-groups"
}

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>research-map · doxa-research-map</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Research map — MapLibre (token-free, no search bar) · doxa-research-map</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -51,7 +51,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Adoption Map · doxa-simple-map</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Engagement Map · doxa-simple-map</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>doxa-simple-map · doxa-simple-map</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Prayer Map · doxa-simple-map</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Gospel x Internet — the board · gospel-internet</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>board · gospel-internet</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>India and China by state and province · gospel-internet</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Language tree — which languages have the gospel, family by family · gospel-internet</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Platforms — which apps would carry a resource there · gospel-internet</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Project 700 — the list as a partner embed · gospel-internet</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>Nothing in their own language — the front door · gospel-internet</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -55,7 +55,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -46,7 +46,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

View file

@ -11,7 +11,7 @@
<script>window.MAP_OWNS_PAGE = true;</script>
<title>gospel-dashboard · gospel-resources</title>
<link rel="icon" href="data:,">
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
@ -60,7 +60,7 @@
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its

Some files were not shown because too many files have changed in this diff Show more