Compare commits

..

No commits in common. "main" and "deep-research-plugin/view-address-p2" have entirely different histories.

143 changed files with 2266 additions and 3257 deletions

147
README.md
View file

@ -37,9 +37,8 @@ Built with love, with the prayer that these maps bless the nations.
**A blessing for you.** These maps and posters are given to bless the people who bless the nations.
We love you, and we are thankful for your work. We bless you in Jesus' name.
Posterverse (`plugin-designs/posterverse/`) brings the six poster plugins together: UPG100 Posters, Country
Posters, Poster Design, Wall of People Groups, Bible Posters and Bless India Posters. They are all in the
Plugin Studio (`bun run dev`) too.
Every poster is in Posterverse (`plugin-designs/posterverse/`): UPG100 Posters, Country Posters,
Poster Design and Wall of People Groups. They are all in the Plugin Studio (`bun run dev`) too.
# dx-maps — one build, maps that embed everywhere
@ -61,7 +60,7 @@ capabilities, three outputs, one shared library, one build.
| 🔗 **Maps that talk to each other** | Maps share props and events, so one app can nest them and a click in one can move another. |
| 🪶 **Small bundles** | Each map's file carries only the parts of the shared library it uses (tree-shaken). |
| ⚙️ **One build step** | `bun run build` makes every map at once. |
| 📦 **Three outputs** | A web folder (iframe, script embed), a mobile app, and a desktop app built from the same code you run in the browser — [the 3 outputs](#what-the-build-makes--3-outputs). |
| 📦 **Three outputs** | A web folder (iframe, script embed), a mobile app, a desktop app — [the 3 outputs](#what-the-build-makes--3-outputs). |
| What dx-maps set out to do | Proof |
|---|---|
@ -71,7 +70,7 @@ capabilities, three outputs, one shared library, one build.
| **One app nests every map** — and each stays embeddable on its own | [capability 5](#capability-5--app-shell-the-research-gallery) · `bun run check:composite-reg` |
| **5 capabilities, every one shippable in every output** | `bun run verify:5-caps` |
| **Shared library, private plugins** — reuse without breaking each other | `bun run check:library-index` |
| **One agent per plugin, the repo agent reads every change, one merge at a time** | [agent swarm](#build-it-with-an-agent-swarm) |
| **One owner per plugin, a second reader for every change, one merge at a time** | [agent swarm](#build-it-with-an-agent-swarm) |
## For vibe coders: build, open the Studio, embed a map
@ -126,9 +125,9 @@ people groups of the world. When you work here, the goal is one of these:
library and the checks.
- **One plugin per folder** in `plugin-designs/`; shared code goes in `library/` only if it stays reusable for
every other plugin.
- **Join `development`** (the pilot), one change at a time through the lock, with `gates:pre`, `gates:post` and the landing checks passing. `main` is
- **Land on `development`**, one change at a time through the lock, with `bun run gates:pre` passing. `main` is
production and moves only on a publish.
- These 8 maps are in production: the 7 production maps in the gallery, plus the country map for DX Life's country pages. The
- These 8 maps are in production: the 7 in the gallery, plus the country map for DX Life's country pages. The
other maps are experimental: leave them as they are unless your task is one
of them.
@ -136,12 +135,11 @@ people groups of the world. When you work here, the goal is one of these:
---
## The production maps
## The 7 production maps
**Seven production maps DX uses today, all together in the Research Gallery**, which DX Life publishes
as **Deep Research**; each one is also a plugin you can embed on its own. The gallery also carries two
poster plugins still marked experimental, Country Posters and Poster Design (see Posterverse). An eighth
production map, the country map (`country-map`), draws DX Life's country pages; it isn't in the gallery.
**Seven maps DX uses today** — each one a plugin you can embed on its own, and all seven together in
the Research Gallery, which DX Life publishes as **Deep Research**. An eighth production map, the country map
(`country-map`), draws DX Life's country pages; it isn't in the gallery.
<table>
<tr>
@ -154,7 +152,7 @@ production map, the country map (`country-map`), draws DX Life's country pages;
<td width="25%" valign="top"><img src="documentation/readme-images/cap4-upg100-mfe.png" alt="UPG100 Posters"><br><b>UPG100 Posters</b><br><sub>prayer lists of 100 people groups, with posters to print</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/prod-cities-internet.png" alt="Cities with Internet"><br><b>Cities with Internet</b><br><sub>internet access where unreached people groups live</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/prod-gospel-resources.png" alt="Gospel Resources"><br><b>Gospel Resources</b><br><sub>which people groups have scripture, film or audio — and which have nothing</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/cap5-doxa-maps-spa.png" alt="Research Gallery"><br><b>Research Gallery</b><br><sub>all nine in one app — capability 5</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/cap5-doxa-maps-spa.png" alt="Research Gallery"><br><b>Research Gallery</b><br><sub>all seven in one app — capability 5</sub></td>
</tr>
</table>
@ -261,9 +259,10 @@ If you build with AI agents, this repo is made for you:
updating the moment you save.
- **Speak instead of typing.** Rudder, an offline voice-to-text tool, sits as a corner widget in the
Studio: look at a plugin, say what you see, and hand the text to your agent. Nothing leaves your
computer. It also runs as its own desktop app.
- **One plugin, several outputs.** The same code ships as a web embed, a mobile app or a desktop app
([the desktop app](#output-3--the-desktop-app--works-today)).
computer (Linux today).
- **One plugin, several outputs.** The same plugin ships as a web embed or a mobile app, and a plugin
can target the desktop: Rudder is the desktop example, a corner widget in the Studio and its own
desktop app (`bun run desktop:build rudder-voice`, see [HOWTO-build-the-desktop-app](./documentation/HOWTO-build-the-desktop-app.md)).
- **Build together.** [Run it in the cloud](#run-it-in-the-cloud) gives a whole team one shared dev
server.
@ -327,7 +326,7 @@ improvement on the next build.
</details>
<details>
<summary><b>What the Research Gallery is made of — seven production maps, two poster plugins, an Experimental shelf, all five capabilities in one plugin</b></summary>
<summary><b>What the Research Gallery is made of — seven production maps, an Experimental shelf, all five capabilities in one plugin</b></summary>
```text
Research Gallery <doxa-maps-spa> capability 5
@ -342,12 +341,10 @@ Research Gallery <doxa-maps-spa> capability 5
│ ├─ regions list regions-list capability 1
│ └─ region dashboards region-dashboards capability 1
├─ Prayer Mobilization
│ ├─ Prayer Progress doxa-simple-map capability 2
│ ├─ UPG100 Posters upg-100-mfe capability 4
│ │ ├─ zoned map upg-100-list capability 1
│ │ └─ poster wall poster-wall capability 1
│ ├─ Country Posters country-posters-mfe capability 4
│ └─ Poster Design poster-design-mfe capability 4
│ └─ Prayer Progress doxa-simple-map capability 2
├─ Gospel Research
│ ├─ Cities with Internet connectivity-map capability 1
│ └─ Gospel Resources resources-map capability 1
@ -355,6 +352,7 @@ Research Gallery <doxa-maps-spa> capability 5
├─ prayer walk planner prayer-walk-planner capability 1
├─ UPG neighbor upg-neighbor capability 3
├─ all people groups all-people-groups capability 1
├─ country posters country-posters capability 4
├─ vision card doxa-vision-card capability 2
├─ PPLR data maps pplr-data-maps capability 3
└─ Wall of People Groups wall-of-people-groups capability 3
@ -500,7 +498,7 @@ files in the repo. The `Dockerfile` and `compose.yaml` sit at the repo root; rec
**Plugin Studio: the public Studio, with PocketBase.** The highest route: the same container with
`ACCESS=public`, on your research subdomain.
- The Plugin Studio follows `development` and is open to everyone, so what agents add to the pilot shows up live.
- The Plugin Studio follows `development` and is open to everyone, so what agents land shows up live.
- Its `/cdn`, built from `main`, serves your site's production maps.
- Publishing and PocketBase's dashboard ask for a password you set when you deploy; OIDC stays available.
@ -617,38 +615,21 @@ bun run phone # builds the app and installs it on a phone or emulator
### Output 3 — the desktop app ✅ works today
**Run it in your browser, then ship the same code as a desktop app.** [Tauri](https://tauri.app) wraps
it in a small Rust program that runs on your computer. Rudder, the offline voice-to-text plugin, is the
proof: its Linux desktop app works and is used to dictate the work on many of these maps. Its goal is
private voice-to-text that you own: offline, no telemetry, no cloud.
**We got the JAMstack map app running as a desktop app.**
![Desktop app (Tauri), working](documentation/readme-images/deploy5-desktop.png)
**Rudder, the voice-to-text plugin, runs as its own desktop app too**, built with
`bun run desktop:build rudder-voice` ([HOWTO-build-the-desktop-app](./documentation/HOWTO-build-the-desktop-app.md)).
![Rudder, the offline voice-to-text desktop app, running](documentation/readme-images/deploy5-desktop-rudder.png)
<details>
<summary><b>More: how it works, Rudder's goal, and what isn't run yet</b></summary>
<summary><b>More about the desktop app</b></summary>
**How it works.** What you build and try on the development server (`bun run dev`) is what ships to
the desktop; there is no second code base. Rudder runs as a corner widget in the Plugin Studio and as
its own desktop app, built with `bun run desktop:build rudder-voice`
([HOWTO-build-the-desktop-app](./documentation/HOWTO-build-the-desktop-app.md)): look at a plugin, say
what you see, and hand the text to an agent. The map app runs as a desktop app the same way.
**Rudder's goal, from the start: private voice-to-text that you own.**
- **Offline, with no telemetry and no cloud.** Your own code and your own self-hosted speech model
(NVIDIA's open NeMo-Speech.cpp runtime and a small speech model, about 742 MB) run on a modest amount
of your computer's CPU. After a one-time download of the runtime and the model, nothing needs the
network; a trace of the speech runtime showed zero sockets and zero DNS lookups. The aim is that it
works out of the box: today the first-run download is still a manual step.
- **Be careful who commits to Rudder.** A voice-to-text tool hears everything said near it, so one
bad change could turn it into spyware. Take changes to `plugin-designs/rudder-voice/` and
`backend/rudder-engine/` only from people you trust, and have a second person read every one.
- **Planned: an egress monitor** that shows you, as it runs, that Rudder sends no data out to anyone.
**What isn't run yet.** Tauri builds a Linux app and a `.deb` installer today. The macOS and Windows
builds are set up but have not been run ([the HOWTO](./documentation/HOWTO-build-the-desktop-app.md)
says which steps are proven). The desktop build runs on demand with the commands below; the automatic
production build doesn't make it yet. The Research Gallery could become a strong desktop app too.
[Tauri](https://tauri.app) wraps the same built folder into a desktop program: it produced a Linux
app and a `.deb` installer. It isn't in the dx bundler for production yet — and the Research Gallery
could become a strong desktop app.
```bash
bun run check:tauri # checks the desktop setup — needs no Rust tools
@ -744,9 +725,8 @@ has one agent that owns it, and the work moves through three kinds of branch:
with hot reload follows it, so each change that joins can be seen there within about 15 seconds.
- **The landing branch (`main`)** is what ships. It moves only when a person approves a publish, and
the landing server rebuilds from it.
- **PR branches** hold one plugin or feature each. They start from the pilot branch, take in the latest
pilot by merging (never rebasing), and join it one at a time, after the checks pass and a second agent,
usually the repository's agent, has read the change.
- **PR branches** hold one plugin or feature each. They start from the landing branch and join the
pilot branch, one at a time, after the checks pass and a second reader has read the change.
**It scales by adding PR branches, not by sharing one.** Many people, each with several agents, can
work on many plugins at the same time: every plugin or feature gets its own PR branch, and each one
@ -754,12 +734,10 @@ can be seen on the pilot server as soon as it joins the pilot.
**Bring your own harness.** dx-maps works with whatever AI coding agent or agent harness you already
use. The repo gives you the branch strategy below, the checks (`bun run gates:pre`) and the Plugin
Studio; it does not ship a harness. There are three roles: one agent per repository, which handles its
branches and watches the work being done in it; one agent per plugin; and one shepherd agent that all the
other agents talk to, which keeps order and puts the merges onto the pilot in order. Your harness gives
these jobs to its agents.
Studio; it does not ship a harness. Roles such as a coordinator that puts the merges onto the pilot in order, or a
gate-keeper that reads each change before it joins the pilot, are jobs your harness gives to its agents.
![The delegation model: the pilot branch (development) at the centre behind the pilot lock; publishing to the landing branch (main) and its outputs below it; inside the Plugin Studio; eight plugins around it, each owned by one agent; a shepherd and a repo agent over the plugin agents; below, how a change joins the pilot and then reaches the landing branch, in four steps, the three outputs, and how it scales: one PR branch per plugin, a self-hosted git service, and publishing to a public host only from the landing branch](documentation/readme-images/delegation-model.png)
![The delegation model: the pilot branch (development) at the centre behind the pilot lock; publishing to the landing branch (main) and its outputs below it; inside the Plugin Studio; eight plugins around it, each owned by one agent; a coordinator and a gate-keeper over the agents; below, how a change joins the pilot and then reaches the landing branch, in four steps, the three outputs, and how it scales: one PR branch per plugin, a self-hosted git service, and publishing to a public host only from the landing branch](documentation/readme-images/delegation-model.png)
<sub>[Open the diagram full size](documentation/readme-images/delegation-model.png): on a phone its small text needs it.</sub>
@ -770,11 +748,11 @@ these jobs to its agents.
```bash
git fetch origin && git switch -c development origin/main && git push -u origin development
```
3. **One PR branch and worktree per feature**, each started from the pilot branch, then install
3. **One PR branch and worktree per feature**, each started from the landing branch, then install
once in each:
```bash
git worktree add -b regions/work ../dx-maps-regions origin/development
git worktree add -b upg100/work ../dx-maps-upg100 origin/development
git worktree add -b regions/work ../dx-maps-regions origin/main
git worktree add -b upg100/work ../dx-maps-upg100 origin/main
cd ../dx-maps-regions && bun install --frozen-lockfile
```
4. **One preview server per agent on your own machine, each on its own port** (in the cloud, the one
@ -782,43 +760,35 @@ these jobs to its agents.
```bash
bun x vite --port 5301 --strictPort # the next agent uses 5302, and so on
```
5. **Join the pilot one at a time, through a lock that works like a merge queue.** First merge the
latest pilot into your PR branch (merge, never rebase) and fix any clash there. A second agent reads
the change, and looks at it on screen where it shows. Then, inside the lock, merge the pilot once more
in case it moved, run the checks (`gates:pre`, `gates:post`) and the landing checks (fresh bundles,
the tree, the integrity hashes, no conflict markers), and push only if nothing fails:
5. **Join the pilot one at a time, through a lock that works like a merge queue.** Inside the lock, merge your PR branch into a
throwaway copy of the pilot branch, run the checks, and push that merge only if nothing fails
(`gates:pre` exits non-zero when a check fails, so the push never runs then). Your PR branch itself
stays one feature on the landing branch; only the pilot gets the merge:
```bash
git fetch origin && git merge --no-edit origin/development # on your PR branch: the latest pilot
flock /tmp/dx-maps-development.lock sh -c '
git fetch origin && git merge --no-edit origin/development &&
bun run gates:pre && bun run gates:post &&
node internal/landing-preconditions.mjs &&
git push origin HEAD:development'
git fetch origin && git switch --detach origin/development &&
git merge --no-edit regions/work &&
bun run gates:pre && git push origin HEAD:development
git switch regions/work'
```
If the merge stops on a conflict, run `git merge --abort`, fix the clash on your PR branch and join
again. `flock` guards one machine. Agents on several machines need a shared lock, such as one your
harness provides.
6. **Each change rebuilds what it touches.** There is no single build owner: rebuild every bundle your
change reaches, then the tree and the integrity hashes, and commit them with the change. The landing
checks refuse a change whose bundles are stale:
6. **One agent owns the full build**; the others build only their own plugin:
```bash
BUNDLE=<name> bun x vite build # each bundle your change reaches
bun run tree && bun run fix:sri # then the tree and the integrity hashes
BUILD_ALLOW=1 bun run build # the build owner: every plugin
BUNDLE=<name> bun x vite build && bun run tree && bun run fix:sri # everyone else: one plugin
```
Only a change to the build itself (its config or its dependencies) reaches every bundle:
`BUILD_ALLOW=1 bun run build`.
7. **Publish, when a person approves it:** make sure the release the landing branch is on now has its
own version tag (that tag is the rollback point), fast-forward the landing branch to a pilot commit
that passed the checks, tag it, and deploy from the tag:
7. **Publish, when a person approves it:** fast-forward the landing branch to a pilot commit that
passed the checks, tag it, and deploy from the tag:
```bash
git fetch origin && git switch main && git merge --ff-only origin/main
git tag -a v0.2.0 -m "Release v0.2.0" && git push origin v0.2.0 # the outgoing release, if untagged
git merge --ff-only <development-commit> # a commit that passed the checks
bun run gates:pre && bun run gates:post # the checks must pass on exactly this commit
bun run gates:pre # the checks must pass on exactly this commit
git tag -a v1.0.0 -m "Release v1.0.0" && git push origin main v1.0.0
```
The tag's `public/dx-maps/` folder is the built site: deploy that, and build the landing demo from
the same tag ([how to deploy](./documentation/DEPLOY.md)). To go back, deploy the previous release's tag. Publish to a public host, such as GitHub, only from
The tag's `public/dx-maps/` folder is the built site: deploy that
([how to deploy](./documentation/DEPLOY.md)). Publish to a public host, such as GitHub, only from
the landing branch and only when it is ready.
### Developing in the cloud with a swarm
@ -856,9 +826,8 @@ So the project works like a busy kitchen:
2. **Only one change joins the pilot at a time.** Whoever holds the *pilot lock* first catches up with
everyone else's latest work, fixes any clash itself and runs every check; the change goes into the
pilot branch only if nothing fails. The pilot branch always works.
3. **Someone keeps order.** One shepherd agent, which all the other agents talk to, keeps order and puts
the merges onto the pilot in order; one agent per repository handles its branches and watches the work
being done in it. Your harness gives these jobs to its agents.
3. **Someone keeps order.** A coordinator can put the merges onto the pilot in order, and a gate-keeper can read
each change by its content before it joins the pilot. Your harness gives these jobs to its agents.
4. **The landing branch moves only when a person approves a publish**, after the checks pass on that exact version.
The pilot server follows the pilot branch; the landing server rebuilds from the landing branch.
@ -876,7 +845,7 @@ That's how many plugins get built at the same time, in one project, without the
| **main** (the landing) | the version that ships: it moves only when a person approves publishing `development` |
| **worktree** | a private copy of the project for one agent to work in |
| **merge conflict** | two changes touched the same lines; someone has to decide how to combine them |
| **merge** | taking everyone else's latest work into your branch: the latest pilot goes into your PR branch (never a rebase), before joining and once more inside the pilot lock |
| **merge** | combining your branch with everyone else's latest work; here it happens inside the pilot lock, in a throwaway copy of the pilot |
| **pilot lock** | only one change merges onto `development` at a time; its holder catches up and runs every check first |
| **tag** | a fixed name for one release commit, such as `v1.0.0`, so you can always deploy exactly it |
@ -960,7 +929,7 @@ The studio is a plugin like any other: `plugin-designs/plugin-studio/plugin-stud
### Run it in the cloud
**One shared dev server for the whole team, no laptop setup.** A container clones the repo, follows
`development`, and hot-reloads every change that joins the pilot, so everyone sees the Plugin Studio update in
`development`, and hot-reloads every change an agent lands, so everyone sees the Plugin Studio update in
their browser within seconds, with no rebuild. Add OIDC settings and it puts a "Sign in with *your
provider*" page in front of everything, open only to the groups you allow. Recipe and settings:
[deployment/cloud/](./deployment/cloud/README.md).
@ -971,7 +940,7 @@ The three ways dx-maps ships (a drop-in into your own site, the container on its
### Cloud development environment
**Your laptop can follow what agents add to the pilot somewhere else.** By default `bun run dev` is fully local: no
**Your laptop can follow what agents land somewhere else.** By default `bun run dev` is fully local: no
settings, nothing fetched, and only your own edits hot-reload. To also see what agents push to a shared
branch (an agent on a server, or the whole team), add two lines to `.env.local` (gitignored):

View file

@ -4,9 +4,8 @@ Rudder (`plugin-designs/rudder-voice`) is voice-to-text that runs on your own co
local engine (`backend/rudder-engine`, Go) that drives NVIDIA's NeMo-Speech.cpp speech server. The
desktop app is a Tauri window around the built web tree, with the engine started as a **sidecar**.
This page is written for an agent following it cold. **Read "What is proven" first**: the Linux app is
proven and in use; the macOS and Windows path has not been run yet, and the steps say so where that is
the case.
This page is written for an agent following it cold. **Read "What is proven" first**: most of the
desktop path has never been run by anyone, and the steps say so where that is the case.
## What is proven, and what is not
@ -19,8 +18,8 @@ the case.
| The engine **compiling** for macOS (arm64, x86_64) and Windows (x86_64) | Proven by cross-compiling. It has **never been run** on either. |
| The engine accepting a WebSocket from the Tauri webview (`tauri://localhost`, `http://tauri.localhost`) | Proven against a running engine; another site's origin is refused. |
| NeMo-Speech.cpp runtime for macOS / Windows | Published by NVIDIA (release v0.1.0: `macos-aarch64-cpu`, `macos-aarch64-metal`, `macos-x86_64-cpu`, `windows-x86_64-cpu` / `-cuda` / `-vulkan`). **Never run here.** |
| `bun run desktop:build rudder-voice` on **Linux** | Proven: produced `Rudder_0.1.0_amd64.deb` (8.8 MB) holding the app and the `rudder-engine` sidecar side by side. The installed Linux app has since been launched and is in use: it is used to dictate the work on these maps. |
| The shell starting and stopping the engine sidecar | Built (`deployment/src-tauri/src/main.rs`), compiled into that installer. The installed Linux app works (the row above); not yet run on macOS or Windows. |
| `bun run desktop:build rudder-voice` on **Linux** | Proven: produced `Rudder_0.1.0_amd64.deb` (8.8 MB) holding the app and the `rudder-engine` sidecar side by side. The app has **not been launched** from it yet (no display on the build box). |
| The shell starting and stopping the engine sidecar | Built (`deployment/src-tauri/src/main.rs`), compiled into that installer; not yet run. |
| First-run model download | **Not built.** Run `setup-runtime.sh` into the app's data folder by hand (step 7). |
## 1. Prerequisites

Binary file not shown.

Before

Width:  |  Height:  |  Size: 931 KiB

After

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View file

@ -116,15 +116,15 @@
<text x="614.1" y="556.0" text-anchor="middle" font-size="12" font-weight="800" fill="#155e75">Experimental</text>
<text x="614.1" y="570.0" text-anchor="middle" font-size="10.5" fill="#155e75">shelf · 5 plugins</text>
<circle cx="543.4" cy="351.1" r="40" fill="#ecfeff" stroke="#0891b2" stroke-width="2.5"/>
<text x="543.4" y="349.1" text-anchor="middle" font-size="12" font-weight="700" fill="#155e75">an agent</text>
<text x="543.4" y="364.1" text-anchor="middle" font-size="12" font-weight="700" fill="#155e75">per plugin</text>
<text x="543.4" y="349.1" text-anchor="middle" font-size="12" font-weight="700" fill="#155e75">roaming</text>
<text x="543.4" y="364.1" text-anchor="middle" font-size="12" font-weight="700" fill="#155e75">agent</text>
<circle cx="690" cy="248" r="56" fill="#f5f3ff" stroke="#7c3aed" stroke-width="3.5"/>
<text x="690" y="254" text-anchor="middle" font-size="14" font-weight="800" fill="#4c1d95">shepherd</text>
<text x="690" y="254" text-anchor="middle" font-size="14" font-weight="800" fill="#4c1d95">coordinator</text>
<circle cx="812" cy="248" r="56" fill="#f1f5f9" stroke="#334155" stroke-width="3.5"/>
<text x="812" y="254" text-anchor="middle" font-size="14" font-weight="800" fill="#1e293b">repo agent</text>
<text x="884" y="192" font-size="17" fill="#4c1d95">one shepherd: every agent talks to it; it orders the merges;</text>
<text x="884" y="216" font-size="17" fill="#1e293b">a repo agent per repo runs its branches and watches the work</text>
<text x="750" y="1502" text-anchor="middle" font-size="20" font-weight="800" fill="#1e3a8a">one agent per plugin, in its own branch · the repo agent reads every change</text>
<text x="812" y="254" text-anchor="middle" font-size="14" font-weight="800" fill="#1e293b">gate-keeper</text>
<text x="884" y="192" font-size="17" fill="#4c1d95">a coordinator puts the merges in order;</text>
<text x="884" y="216" font-size="17" fill="#1e293b">a gate-keeper checks each one by its content</text>
<text x="750" y="1502" text-anchor="middle" font-size="20" font-weight="800" fill="#1e3a8a">one owner per plugin, in its own branch · a second reader for every change</text>
<rect x="36" y="110" width="304" height="280" rx="14" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2"/>
<circle cx="64" cy="142" r="11" fill="#166534" stroke="#14532d" stroke-width="2.5"/>
<text x="88" y="147" font-size="15" fill="#334155">pilot branch (development)</text>
@ -139,9 +139,9 @@
<circle cx="64" cy="302" r="11" fill="#eff6ff" stroke="#2563eb" stroke-width="2.5"/>
<text x="88" y="307" font-size="15" fill="#334155">agent — owns one plugin</text>
<circle cx="64" cy="334" r="11" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2.5"/>
<text x="88" y="339" font-size="15" fill="#334155">shepherd — orders merges</text>
<text x="88" y="339" font-size="15" fill="#334155">coordinator — orders merges</text>
<circle cx="64" cy="366" r="11" fill="#f1f5f9" stroke="#334155" stroke-width="2.5"/>
<text x="88" y="371" font-size="15" fill="#334155">repo agent — one per repo</text>
<text x="88" y="371" font-size="15" fill="#334155">gate-keeper — checks each merge</text>
<rect x="60" y="1536" width="1380" height="232" rx="20" fill="#fef2f2" stroke="#fca5a5" stroke-width="2"/>
<text x="750" y="1578" text-anchor="middle" font-size="22" font-weight="800" fill="#7f1d1d">How a change joins the pilot — one at a time, so many plugins can be built at once</text>
<rect x="80.0" y="1600" width="305" height="148" rx="14" fill="#fff" stroke="#dc2626" stroke-width="2"/>
@ -155,8 +155,8 @@
<rect x="425.0" y="1600" width="305" height="148" rx="14" fill="#fff" stroke="#dc2626" stroke-width="2"/>
<circle cx="459.0" cy="1634" r="18" fill="#dc2626"/>
<text x="459.0" y="1641" text-anchor="middle" font-size="19" font-weight="800" fill="#fff">2</text>
<text x="487.0" y="1641" font-size="19" font-weight="800" fill="#0f172a">Repo agent reads</text>
<text x="445.0" y="1680" font-size="15" fill="#475569">the repo agent reads every</text>
<text x="487.0" y="1641" font-size="19" font-weight="800" fill="#0f172a">Second reader</text>
<text x="445.0" y="1680" font-size="15" fill="#475569">another agent reads every</text>
<text x="445.0" y="1702" font-size="15" fill="#475569">change by its content</text>
<text x="445.0" y="1724" font-size="15" fill="#475569">before it may join</text>
<text x="750.0" y="1682" text-anchor="middle" font-size="26" font-weight="800" fill="#dc2626">→</text>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" font-family="Inter, 'Helvetica Neue', Arial, sans-serif">
<rect width="1200" height="630" rx="28" fill="#f8fafc"/>
<rect x="1.5" y="1.5" width="1197" height="627" rx="27" fill="none" stroke="#e2e8f0" stroke-width="3"/>
<g transform="translate(110,175) scale(11.5)"><path d="M13.912 0a8.72 8.72 0 0 0-8.308 6.139c1.05-.515 2.18-.845 3.342-.976 2.415-3.363 7.4-3.412 9.88-.097 2.48 3.315 1.025 8.084-2.883 9.45a6.131 6.131 0 0 1-.3 2.762 8.72 8.72 0 0 0 3.01-1.225A8.72 8.72 0 0 0 13.913 0zm.082 6.451a2.284 2.284 0 1 0-.15 4.566 2.284 2.284 0 0 0 .15-4.566zm-5.629.27a8.72 8.72 0 0 0-3.031 1.235 8.72 8.72 0 1 0 13.06 9.9131 10.173 10.174 0 0 1-3.343.965 6.125 6.125 0 1 1-7.028-9.343 6.114 6.115 0 0 1 .342-2.772zm1.713 6.27a2.284 2.284 0 0 0-2.284 2.283 2.284 2.284 0 0 0 2.284 2.284 2.284 2.284 0 0 0 2.284-2.284 2.284 2.284 0 0 0-2.284-2.284z" fill="#24C8DB"/></g>
<text x="480" y="250" font-size="54" font-weight="800" fill="#0f172a">Desktop app (Tauri)</text>
<text x="480" y="315" font-size="27" fill="#475569">We got the JAMstack map app running</text>
<text x="480" y="355" font-size="27" fill="#475569">as a desktop app.</text>
<rect x="480" y="400" width="420" height="62" rx="31" fill="#fef3c7" stroke="#d97706" stroke-width="2.5"/>
<text x="690" y="441" text-anchor="middle" font-size="26" font-weight="800" fill="#92400e">PROVEN · NOT YET LIVE</text>
<text x="480" y="508" font-size="22" fill="#64748b">Proven, not yet in production. The Research</text>
<text x="480" y="538" font-size="22" fill="#64748b">Gallery could become a strong desktop app.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -280,8 +280,6 @@ and a change here can break other maps.
`import … from '@map/utils/peopleGroupName.js'`
- **peopleGroupPoints** — peopleGroupPoints.js — SHARED (@map/utils/peopleGroupPoints.js).
`import … from '@map/utils/peopleGroupPoints.js'`
- **publicEmbed** — publicEmbed.js: the Share menu's Embed codes for THIS plugin, from the tree's PUBLIC address.
`import … from '@map/utils/publicEmbed.js'`
- **scrollport** — findScrollportAncestor — the nearest ancestor that actually scrolls.
`import … from '@map/utils/scrollport.js'`
- **viewportScrim** — viewportScrim.js — make a position:fixed full-screen SCRIM actually cover the VIEWPORT,

View file

@ -50,19 +50,6 @@ function readGated() {
}
export const GATED_PROFILES = readGated()
// The PUBLIC address of the tree for the Share menu's Embed tab (the config's shareBaseUrl): an https URL with no
// trailing slash, or '' (unset). vite.config.js bakes it as __DOXA_SHARE_BASE__; library/utils/embedBase.js reads it.
function readShareBase() {
let v = ''
try {
const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, 'utf8'))
v = typeof cfg.shareBaseUrl === 'string' ? cfg.shareBaseUrl.trim().replace(/\/+$/, '') : ''
} catch { return '' }
if (v && !/^https:\/\/[^\s"'<>]+$/i.test(v)) throw new Error(`[_tree-name] build config "shareBaseUrl" must be an https URL, got ${JSON.stringify(v)}`)
return v
}
export const SHARE_BASE = readShareBase()
// The name escaped for use inside a RegExp source (a '.' in a future name must not match any char).
export const TREE_NAME_RE = TREE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')

View file

@ -1,136 +0,0 @@
#!/usr/bin/env node
/**
* check-dev-leaf-escape.mjs — a dev leaf URL cannot write markup into the page it opens.
*
* THE DEFECT. The dev server's leaf route, /__leaf/<bundle>/<profile>[/<instance>] (and a production embed URL,
* <bundle>/<profile>[/<instance>]/index.html, rewritten onto it), writes the URL's parts into the page it serves: the
* <title> and the element's profile-config='…' attribute. It refused only `..` and `/`, so a quote or a tag in the path
* was written raw, and a crafted link broke out of the attribute on a publicly reachable dev server.
* internal/plugin-gallery/dev-plugin.mjs now refuses any part outside LEAF_PART (the shape every bundle folder, profile
* id and instance id has) before either sink, and escapes the <title> as well.
*
* The plugin's own middleware answers each request (no server, no browser):
* A. every leaf the dev manifest advertises (galleryModel), as /__leaf/… AND as its production embed URL → 200 with
* its element: the rule refuses no real page;
* B. a hostile part (a quote, a double quote, a tag, an ampersand, a space, a backtick, an encoded slash or
* backslash, `..`, `.`, an attribute break-out) in each position, both URL forms → refused here (400) or left to
* Vite, never a page;
* C. escHtml leaves none of & < > " ' raw;
* D. with the part rule set back to the old one (`..` and `/` only), a tag in the profile still reaches the <title>
* escaped: the title has its own escaping, not only the rule in front of it.
* Response bodies are never printed: a leaf page carries the build config's token.
* `--self-test`: the old guard, a rule too strict for a real profile, an unescaped <title> and an escHtml that misses
* the quote must each FAIL.
*/
import { readFileSync, writeFileSync, rmSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { discoverSpaRegistry } from './plugin-gallery/discover.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const PLUGIN = join(HERE, 'plugin-gallery/dev-plugin.mjs')
const SRC = readFileSync(PLUGIN, 'utf8')
const GUARD = 'parts.some(s => !LEAF_PART.test(s))'
const OLD_GUARD = "parts.some(s => !s || s.includes('..') || s.includes('/'))"
const RULE = 'export const LEAF_PART = /^[A-Za-z0-9][\\w-]{0,63}$/'
const TITLE = '${escHtml(bundle)} · ${escHtml(profile)}'
const ESC_CLASS = `/[&<>"']/g`
// A copy of the plugin, altered or not, imported from beside the original so its relative imports resolve; the
// file is removed as soon as it is loaded.
let seq = 0
async function load(src) {
if (src === SRC) return import(pathToFileURL(PLUGIN).href)
const tmp = join(HERE, 'plugin-gallery', `.tmp-check-dev-leaf-escape-${process.pid}-${++seq}.mjs`)
writeFileSync(tmp, src)
try { return await import(pathToFileURL(tmp).href) } finally { rmSync(tmp, { force: true }) }
}
// The plugin's middleware, answering one GET: { status, body } ('next' = not answered here, Vite would).
function client(mod) {
let mw = null
mod.autoSpaDevPlugin('').configureServer({ middlewares: { use: (f) => { if (!mw) mw = f } } })
return (url) => new Promise((done) => {
const res = { statusCode: 200, setHeader() {}, end: (b) => done({ status: res.statusCode, body: String(b ?? '') }) }
try { mw({ url, method: 'GET', headers: {} }, res, () => done({ status: 'next', body: '' })) }
catch (e) { done({ status: 'threw', body: String(e && e.message) }) }
})
}
// Every /__leaf/ URL the dev manifest advertises, with the tag its page must mount.
function advertised(mod) {
const out = []
const walk = (o, tag) => {
if (!o || typeof o !== 'object') return
for (const v of Object.values(o)) {
if (typeof v === 'string' && v.startsWith('/__leaf/')) out.push({ url: v.split(/[?#]/)[0], tag })
else walk(v, tag)
}
}
for (const b of mod.galleryModel(discoverSpaRegistry()).bundles) walk(b, (b.elements || [])[0])
return [...new Map(out.map((x) => [x.url, x])).values()]
}
const HOSTILE = ["a'b", 'a"b', 'a<b', 'a>b', 'a&b', 'a b', 'a`b', 'a/b', 'a\\b', '..', '.', "x'><img src=x onerror=alert(1)>"]
async function audit(src) {
const f = []
let mod
try { mod = await load(src) } catch (e) { return [`load: the plugin does not import (${e.message})`] }
const get = client(mod)
const leaves = advertised(mod)
if (leaves.length < 20) f.push(`A: only ${leaves.length} advertised leaves found (the manifest walk is broken)`)
for (const { url, tag } of leaves) {
for (const u of [url, url.replace('/__leaf/', '/') + '/index.html']) {
const r = await get(u)
if (r.status !== 200 || !r.body.includes(`<${tag} `)) f.push(`A: ${u} → ${r.status}${r.status === 200 ? `, no <${tag}>` : ''} (a real page refused)`)
}
}
const [b0, p0] = (leaves[0] ? leaves[0].url : '/__leaf/x/x').slice('/__leaf/'.length).split('/')
for (const h of HOSTILE) {
const e = encodeURIComponent(h)
for (const parts of [[e, p0], [b0, e], [b0, p0, e]]) {
const leaf = '/__leaf/' + parts.join('/')
const r = await get(leaf)
if (r.status !== 400) f.push(`B: ${leaf} → ${r.status} (want 400)`)
const embed = '/' + parts.join('/') + '/index.html'
const r2 = await get(embed)
if (r2.status !== 400 && r2.status !== 'next') f.push(`B: ${embed} → ${r2.status} (want 400, or left to Vite)`)
}
}
const esc = mod.escHtml ? mod.escHtml(`&<>"'`) : null
if (esc !== '&amp;&lt;&gt;&quot;&#39;') f.push(`C: escHtml(& < > " ') = ${JSON.stringify(esc)}`)
if (!src.includes(GUARD)) f.push('D: the leaf guard is not the LEAF_PART rule, so the title cannot be tested behind the old one')
else {
let loose
try { loose = await load(src.replace(GUARD, OLD_GUARD)) } catch (e) { f.push(`D: ${e.message}`) }
if (loose) {
const r = await client(loose)(`/__leaf/${b0}/${encodeURIComponent('<b>x')}`)
const title = (/<title>([^]*?)<\/title>/.exec(r.body) || [])[1]
if (r.status !== 200 || title === undefined) f.push(`D: behind the old rule the page did not render (${r.status})`)
else if (title.includes('<b>') || !title.includes('&lt;b&gt;')) f.push('D: behind the old rule a tag in the profile reached the <title> raw')
}
}
return f
}
if (process.argv.includes('--self-test')) {
const poisons = [
['the old guard (`..` and `/` only)', SRC.replace(GUARD, OLD_GUARD), /^B: /],
['a rule too strict for a real profile', SRC.replace(RULE, 'export const LEAF_PART = /^[a-z]{1,63}$/'), /^A: /],
['an unescaped <title>', SRC.replace(TITLE, '${bundle} · ${profile}'), /^D: /],
['an escHtml that misses the quote', SRC.replace(ESC_CLASS, `/[&<>"]/g`), /^C: /],
]
let missed = 0
for (const [name, src, rx] of poisons) {
if (src === SRC) { console.log(`✗ MISSED: ${name} (not planted: its source line was not found)`); missed++; continue }
const f = await audit(src); const hit = f.find((x) => rx.test(x))
console.log(hit ? `✓ caught: ${name} -> ${hit}` : `✗ MISSED: ${name}${f.length ? ` (failed instead: ${f[0]})` : ''}`)
if (!hit) missed++
}
if (missed) process.exitCode = 1
} else {
const f = await audit(SRC)
if (f.length) { for (const x of f.slice(0, 40)) console.log('✗ ' + x); console.log(`FAIL check:dev-leaf-escape (${f.length})`); process.exitCode = 1 }
else console.log(`PASS check:dev-leaf-escape — every advertised dev leaf is served in both URL forms; ${HOSTILE.length} hostile parts are refused in every position of both forms; the <title> escapes on its own`)
}

View file

@ -37,7 +37,6 @@ const STATIC = [
['check:click-sheet', 'the country sheet resolves, links are absolute'],
['check:snapshot-guard', 'no fetcher writes around the guard'],
['check:gospel-tables-staged', 'every asset the map fetches is in the shipped tree'],
['check:gospel-clear-filter-url', 'Show all drops only the filter keys, not a partner\'s'],
]
const BROWSER = [
['check:leaves-mount', 'all five leaves actually mount'],

View file

@ -68,11 +68,6 @@ const areas = readdirSync(join(ROOT, 'en')).filter((f) => f.endsWith('.json'))
// translations, add the locale here with its count and lower it as they arrive.
// Never raise a number; never add a locale that is already complete.
const DEBT = {
// 2026-10-03, the Share menu's 10 new share.json keys (shareable pages P3): localization is English-only for now.
// tStatic falls back to the English text, so these locales show English until the translations arrive;
// lower each to 0 then, and empty the map again.
// + the Embed tab's 9 keys (URL / Iframe / JavaScript, P3b), the same English-only rule.
ar: 19, de: 19, es: 19, fr: 19, hi: 19, it: 19, pt: 19, ro: 19, ru: 19, zh: 19,
}
const fails = []

View file

@ -39,19 +39,7 @@ const m = code.match(/host\.value\s*=\s*([^\n]+)/)
if (!m) {
fails.push('the area resolution moved — this gate cannot find `host.value = …` and would pass blind.')
} else {
let expr = m[1]
// The resolution may live in a named function (resolveHost(), shareable pages P3: a standalone Share button
// re-resolves at open time). Then judge THAT function's body, and fail if it cannot be found.
const fn = expr.match(/^\s*(\w+)\(\)/)
if (fn) {
const body = code.match(new RegExp('function\\s+' + fn[1] + '\\s*\\(\\)\\s*\\{([\\s\\S]*?)\\n\\}'))
// every return that resolves a named area must carry its structural fallback on the SAME expression
// a same-line comment is not a fallback: strip `//…` before testing each return
const rets = body ? [...body[1].matchAll(/return\s+([^\n]*closest\([^\n]*)/g)].map((r) => r[1].replace(/\s*\/\/.*$/, '')) : []
expr = rets.length && rets.every((r) => /offsetParent|parentElement|document\.body/.test(r)) ? rets.join(' ;; ') : (rets.find((r) => !/offsetParent|parentElement|document\.body/.test(r)) || '')
if (!body) fails.push(`the area resolution calls ${fn[1]}(), and this gate cannot find that function to read it.`)
else if (!rets.length) fails.push(`${fn[1]}() returns no named-area resolution, so this gate would pass blind.`)
}
const expr = m[1]
if (!/offsetParent|parentElement|document\.body/.test(expr)) {
fails.push('MapControlMenu resolves its anchor from a class allowlist with no structural fallback:\n'
+ ` ${expr.trim()}\n`

View file

@ -217,22 +217,6 @@ ok(!/<em>[^<]*<\/em><\/span>/.test(full.slice(full.indexOf('class="card"'))), 't
ok(/10/.test(caps.join(' ')), 'the bounded tally does not state its unknown count — an un-inked category reads as zero in print')
}
// ★ EVERY GROUP OF THE LIST IS COUNTED (2026-10-03): a group with no record in a figure's data is named in
// that figure's caption ("15 without data"), so each caption's parts add up to the list. Both ways: the
// fixture's figures lack records (45 groups; 30 bounded, 24 centered), and complete data prints no such part.
{
const sum = (cap) => [...cap.matchAll(/\d[\d,]*/g)].reduce((a, m) => a + Number(m[0].replace(/,/g, '')), 0)
const capsOf = (html) => [...html.matchAll(/class="set-cap">([^<]*)</g)].map((m) => m[1])
const nd = String(strings.en.sets_no_data).replace('{n}', '').trim()
const short = capsOf(full)
ok(short.length === 2 && short.every((c) => sum(c) === 45 && c.includes(nd)),
`each set caption should add up to the list's 45 groups, naming the ones ${nd}: ${short.join(' | ')}`)
const whole = capsOf(R.buildPacket(zone, groups, new Map(), '<svg/>', funnel, christian, 'full', {
bounded: groups.map((g, i) => ({ id: g.slug, reached: i % 2 === 0 })), centered: groups.map((g, i) => ({ id: g.slug, ring: (i % 10) + 1 })) }))
ok(whole.length === 2 && whole.every((c) => sum(c) === 45 && !c.includes(nd)),
`with a record for every group, each caption should add up to 45 and name none ${nd}: ${whole.join(' | ')}`)
}
// ── THE RING LEGEND: THE AGREED VOCABULARY, AND ONLY THE RINGS THAT HOLD SOMEONE ────────────────
// ★ The names are COMPOSED from band x culture rather than a flat 1..10 table, because "same
// people" (provisional, pending a product decision) appears in rings 1, 4 AND 7 — three cells that a

View file

@ -16,15 +16,13 @@
* B. a cached copy that matches is used as is: ONE request, force-cache (the fast, offline-friendly path stays);
* C. a server that also serves the wrong bytes → rejected, err.integrity, after one fresh refetch;
* D. a file that is not served → rejected (not an integrity failure);
* D2. a page without crypto.subtle (not secure: plain http by LAN address) → refused saying "secure page";
* E. loadPacketStrings with a stale cached table → the full table in every language (not the English fallback);
* F. loadIndiaStates with a stale cached table → India's 36 states (not a rejection);
* G. generateSheet with a stale cached generator → a drawn sheet (not an integrity refusal);
* H. wiring: every pinned loader fetches through fetchPinned with its own pin, and none fetches its pinned file
* directly; the classic-script path refreshes the HTTP cache and retries a refused tag once.
* `--self-test`: fetchPinned without its fresh refetch, one that never uses the cache, one that accepts a
* mismatch, a hash that reaches for crypto.subtle unguarded, and a loader that fetches its pinned file directly must
* each FAIL.
* mismatch, and a loader that fetches its pinned file directly must each FAIL.
* Two-sided by hand at the change: the previous loaders (the pilot before this) fail E, F and G.
*/
import { readFileSync, existsSync } from 'node:fs'
@ -90,12 +88,6 @@ async function auditFetchPinned(shaSrc) {
{ world('https://d.test', { missing: [P] }); let err = null
try { await fetchPinned(`https://d.test/${P}`, pin) } catch (e) { err = e }
if (!err || err.integrity) fails.push(`D: an unserved file should reject as not served (${err ? err.message : 'it resolved'})`) }
// D2: a page that is not secure (plain http by LAN address) has no crypto.subtle: refused in WORDS, not a bare TypeError
{ world('https://e.test', { cached: { [P]: current } }); const realCrypto = globalThis.crypto; let err = null
Object.defineProperty(globalThis, 'crypto', { value: {}, configurable: true, writable: true })
try { await fetchPinned(`https://e.test/${P}`, pin) } catch (e) { err = e }
finally { Object.defineProperty(globalThis, 'crypto', { value: realCrypto, configurable: true, writable: true }) }
if (!err || err.integrity || !/secure page/.test(err.message)) fails.push(`D2: on a page without crypto.subtle the refusal should say "secure page", got ${err ? JSON.stringify(err.message) : 'a resolve'}`) }
globalThis.fetch = realFetch
return fails
}
@ -154,7 +146,6 @@ if (process.argv.includes('--self-test')) {
['fetchPinned without its fresh refetch', () => auditFetchPinned(shaSrc.replace(FOR, "for (const cache of ['force-cache']) {")), /^A: /],
['fetchPinned that never uses the cache', () => auditFetchPinned(shaSrc.replace(FOR, "for (const cache of ['reload', 'reload']) {")), /^B: /],
['fetchPinned that accepts a mismatch', () => auditFetchPinned(shaSrc.replace(' if (got === pin) return text\n', ' return text\n')), /^C: /],
['a hash that reaches for crypto.subtle unguarded', () => auditFetchPinned(shaSrc.replace(/ if \(!subtle\) throw [^\n]*\n/, '')), /^D2: /],
['a loader that fetches its pinned file directly', () => auditWiring((f) => (f === 'library/packet/packetStrings.js'
? read(f).replace('fetchPinned(url, PACKET_STRINGS_SHA384)', "(await fetch(url, { cache: 'force-cache' })).text()") : read(f))), /^H: library\/packet\/packetStrings\.js/],
]
@ -168,5 +159,5 @@ if (process.argv.includes('--self-test')) {
} else {
const f = [...await auditFetchPinned(shaSrc), ...await auditLoaders(), ...auditWiring(read)]
if (f.length) { for (const x of f) console.log('✗ ' + x); console.log(`FAIL check:pinned-fetch-fresh (${f.length})`); process.exitCode = 1 }
else console.log('PASS check:pinned-fetch-fresh — a stale cached copy of a pinned file is refetched once, fresh: page 1 still draws, the packet keeps its language, India keeps its states; a matching cached copy is used as is; wrong bytes are refused; a page that is not secure is told so in words; every pinned loader goes through fetchPinned')
else console.log('PASS check:pinned-fetch-fresh — a stale cached copy of a pinned file is refetched once, fresh: page 1 still draws, the packet keeps its language, India keeps its states; a matching cached copy is used as is; wrong bytes are refused; every pinned loader goes through fetchPinned')
}

View file

@ -16,16 +16,13 @@
// E. ONE WRITER, NESTED: claimView writes for an owner and announces for anyone else (one event per action, view keys
// only); ownAddress records an announcement only while it owns (and stops it there), so a pane and its selection
// clicked together are ONE Back step.
// F. pane-wrapper's opt-in address, on a simulated column: a link opens its pane without writing, also on a FRESH
// load where the column is built before its layout (it waits for a height, measured in a browser); a click is a push
// F. pane-wrapper's opt-in address, on a simulated column: a link opens its pane without writing; a click is a push
// (the first pane is the default and is never written); a click's selection rides in the same push; where the
// reader's scroll settles is a replace; Back/Forward scroll and never write; nested it announces; without the
// opt-in it never writes; a kept-alive column shown again claims its pane back (a replace), unless the address
// names one.
// G. THE WIRING in Regions: regions-mfe opts its column in, owns its leaf, and carries the place with its pane;
// region-dashboards claims ?place= (replace at first paint, push on a choice) and re-applies it on Back. And
// each shell (Plugin Studio, the Research Gallery, Posterverse) records its nested composites' choices
// (ownAddress) and releases the listener before any hot-update return.
// region-dashboards claims ?place= (replace at first paint, push on a choice) and re-applies it on Back.
// --self-test: each poison below must FAIL its arm. Pure node: no Vite, no build, no browser.
import { execFileSync } from 'node:child_process'
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs'
@ -39,6 +36,7 @@ const KNOWN = {
'library/utils/languageRequest.js': [1, '?lang= on a generated page only (MAP_OWNS_PAGE); keeps state and every other param'],
'library/layouts/app-wrapper-material/index.html': [1, "the layout page's own #/<map> hash; a fragment-only URL keeps the query"],
'plugin-designs/gospel-internet/src/useBoard.js': [1, 'its own keys on its own leaves (writeUrl); keeps state, foreign params and the hash'],
'plugin-designs/gospel-resources/data-tables.vue': [1, "clearFilter: replaceState({}, '', pathname) wipes the whole query and hash; reported to its owner"],
}
// SOURCE files only: gates and node scripts may simulate history. A write is `.pushState(` / `.replaceState(` (an aliased
// history included) or a computed `history[… 'pushState' …]`.
@ -89,11 +87,10 @@ const unset = () => { for (const k of ['location', 'history', 'addEventListener'
// A column of panes for pane-wrapper, simulated: each pane is 100 px tall, scrolling dispatches 'scroll'.
class FakeEl extends EventTarget {
constructor() { super(); this.className = ''; this.dataset = {}; this.children = []; this.parent = null; this.scrollTop = 0; this.clientHeight = FakeEl.h0 ?? 100; this.innerHTML = '' }
constructor() { super(); this.className = ''; this.dataset = {}; this.children = []; this.parent = null; this.scrollTop = 0; this.clientHeight = 100; this.innerHTML = '' }
appendChild(c) { c.parent = this; this.children.push(c); return c }
remove() { if (this.parent) this.parent.children = this.parent.children.filter((x) => x !== this) }
// A pane's top is its index times the column's height: 0 while the column has no layout.
get offsetTop() { return this.parent ? this.parent.children.filter((c) => c.className === 'pane').indexOf(this) * this.parent.clientHeight : 0 }
get offsetTop() { return this.parent ? this.parent.children.filter((c) => c.className === 'pane').indexOf(this) * 100 : 0 }
querySelector(sel) {
const id = /data-pane-id="([^"]+)"/.exec(sel)?.[1]
const walk = (n) => { for (const c of n.children) { if (c.className === 'pane' && c.dataset.paneId === id) return c; const r = walk(c); if (r) return r } return null }
@ -136,11 +133,6 @@ async function arms({ va, sv, counts, pw, src }, { skipF = false } = {}) {
const r2 = va.writeViewKeys({ tab: 'regions' }, { owner: 'app', push: true })
if (r2 !== 'unchanged' || b.writes.push + b.writes.replace !== 1) fails.push(`B: an unchanged address wrote (${r2})`)
if (va.commitUrl({}) !== false || b.writes.push + b.writes.replace !== 1) fails.push('B: commitUrl wrote an identical URL')
// the same values in another order is unchanged too (a link written by someone else, a host's own params first)
va.commitUrl({ search: '?owner=x&pane=map&tab=regions' })
const n = b.writes.push + b.writes.replace
const r3 = va.writeViewKeys({ pane: 'map' }, { owner: 'app', push: false })
if (r3 !== 'unchanged' || b.writes.push + b.writes.replace !== n) fails.push(`B: the same keys in another order were rewritten (${r3}): ${b.at()}`)
}
{
const b = browser('https://partner.test/page?tab=theirs')
@ -281,43 +273,6 @@ async function arms({ va, sv, counts, pw, src }, { skipF = false } = {}) {
if (b.listeners.size) fails.push('F: destroy() left its popstate listener')
if (got.length) fails.push('F: an owner composite announced instead of writing')
}
if (!skipF) {
// A FRESH LOAD: the column is built before it has a layout (measured in a browser), and a ResizeObserver's
// FIRST callback comes after layout, already with the full height. The link must still open.
const ros = []
globalThis.ResizeObserver = class { constructor(cb) { this.cb = cb; ros.push(this) } observe() {} disconnect() { this.off = true } }
const b = browser('https://x.test/country-posters-mfe?pane=dashboard&lang=ar')
const host = new FakeEl()
FakeEl.h0 = 0
const c = pw.buildPaneColumn(SPEC, { host, address: { owner: () => 'app' } })
FakeEl.h0 = undefined
const col = host.children[0]
col.dispatchEvent(new Event('scroll')); await settle()
if (b.at() !== '/country-posters-mfe?pane=dashboard&lang=ar') fails.push(`F: before its layout, the column overwrote the link it has not opened yet: ${b.at()}`)
col.clientHeight = 100; ros.forEach((r) => !r.off && r.cb([])); await settle()
if (col.scrollTop !== 100) fails.push(`F: a FRESH LOAD with ?pane=dashboard did not open that pane once the column had its layout (scrollTop ${col.scrollTop})`)
if (b.writes.push + b.writes.replace) fails.push('F: opening a link on a fresh load wrote the address')
c.destroy()
delete globalThis.ResizeObserver
}
if (!skipF) {
// The same fresh load, where the observer NEVER fires again after layout (a pane's height arrived after the
// column's last resize): the bounded rAF loop must still open the link.
const ros = []
globalThis.ResizeObserver = class { constructor(cb) { this.cb = cb; ros.push(this) } observe() {} disconnect() { this.off = true } }
globalThis.requestAnimationFrame = (cb) => setTimeout(cb, 5)
const b = browser('https://x.test/upg-100-mfe?pane=dashboard')
const host = new FakeEl()
FakeEl.h0 = 0
const c = pw.buildPaneColumn(SPEC, { host, address: { owner: () => 'app' } })
FakeEl.h0 = undefined
const col = host.children[0]
col.clientHeight = 100; await settle()
if (col.scrollTop !== 100) fails.push(`F: with an observer that never fires again, the bounded rAF did not open the link (scrollTop ${col.scrollTop})`)
if (b.writes.push + b.writes.replace) fails.push('F: the rAF-opened link wrote the address')
c.destroy()
delete globalThis.ResizeObserver; delete globalThis.requestAnimationFrame
}
if (!skipF) {
// A KEPT-ALIVE column whose tab is hidden and shown again (ResizeObserver: no height, then height).
const ros = []
@ -376,22 +331,6 @@ async function arms({ va, sv, counts, pw, src }, { skipF = false } = {}) {
if (!/claimPlace\(false\)/.test(d) || !/claimPlace\(true\)/.test(d)) fails.push('G: region-dashboards does not replace at first paint and push on a choice')
if (!/onAddressChange\(onAddress\)/.test(d)) fails.push('G: region-dashboards does not re-apply ?place= on Back/Forward')
if (!/new ResizeObserver\([\s\S]{0,400}else if \(!place\) claimPlace\(false\)/.test(d)) fails.push('G: a kept-alive region-dashboards shown again does not claim its place back')
// A composite that opts its column in also owns its own leaf: it records its panes' choices (ownAddress) and
// releases that listener on unmount, or a choice made inside it on its own leaf is never written.
if (!Object.keys(src.opted || {}).length) fails.push('G: no composite opts its pane column into the address (the census of opted composites is empty)')
for (const [f, t] of Object.entries(src.opted || {})) {
if (!/ownAddress\(stage\.value, owner\)/.test(t)) fails.push(`G: ${f} opts its column in but does not record its panes' choices (ownAddress)`)
else if (!/offOwn\?\.\(\)/.test(t)) fails.push(`G: ${f} never releases ownAddress`)
}
// Each shell that owns a page records its nested composites' choices, and releases the listener on unmount
// BEFORE any hot-update early return (or a hot remount stacks a second one).
for (const [f, t] of Object.entries(src.shells || {})) {
if (!/this\._offOwn = ownAddress\(this\.\$el, \(\) => urlOwnerOf\(this\.cfg\)\)/.test(t)) { fails.push(`G: the shell ${f} does not record nested choices (ownAddress)`); continue }
const bu = t.indexOf('beforeUnmount()')
const rel = bu < 0 ? -1 : t.indexOf('this._offOwn()', bu)
const hot = bu < 0 ? -1 : t.indexOf('if (import.meta.hot) return', bu)
if (rel < 0 || (hot >= 0 && hot < rel)) fails.push(`G: the shell ${f} does not release ownAddress in beforeUnmount before any hot-update return`)
}
}
// ── D. the census ──
@ -407,15 +346,7 @@ const va = await import(join(ROOT, 'library/composables/useViewAddress.js'))
const sv = await import(join(ROOT, 'library/composables/useShareableView.js'))
const PW = 'library/layouts/pane-wrapper/pane-wrapper.js'
const pw = await import(join(ROOT, PW))
const SHELLS = ['plugin-studio/plugin-studio.vue', 'doxa-maps-spa/doxa-maps-spa.vue', 'posterverse/posterverse.vue']
// Every composite that opts its pane column into the address, enumerated from the tree.
const OPTED = execFileSync('git', ['grep', '--untracked', '-lE', 'buildPaneColumn\\([^)]*address:', '--', 'plugin-designs', 'library', ':!*.md'], { cwd: ROOT, encoding: 'utf8' }).trim().split('\n').filter(Boolean)
const src = {
mfe: readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/regions-mfe.vue'), 'utf8'),
dash: readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/region-dashboards.vue'), 'utf8'),
shells: Object.fromEntries(SHELLS.map((f) => [f, readFileSync(join(ROOT, 'plugin-designs', f), 'utf8')])),
opted: Object.fromEntries(OPTED.map((f) => [f, readFileSync(join(ROOT, f), 'utf8')])),
}
const src = { mfe: readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/regions-mfe.vue'), 'utf8'), dash: readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/region-dashboards.vue'), 'utf8') }
const counts = census()
const real = { va, sv, counts, pw, src }
// A pane-wrapper with one line changed, loaded from a sibling file so its relative import still resolves.
@ -439,7 +370,6 @@ if (process.argv.includes('--self-test')) {
['a write when not the owner', { ...real, va: { ...va, writeViewKeys: (c, o) => va.writeViewKeys(c, { ...o, owner: 'app' }) } }, /^B: a partner page/],
['a tab change that keeps a stale place', { ...real, va: { ...va, viewSearch: (s, c) => { const k = { ...va.readViewKeys(s), ...Object.fromEntries(Object.entries(c).map(([a, b]) => [a, va.cleanKey(b)])) }; const f = s.replace(/^\?/, '').split('&').filter((p) => p && !va.VIEW_KEYS.includes(p.split('=')[0])); const all = [...va.VIEW_KEYS.filter((x) => k[x]).map((x) => `${x}=${k[x]}`), ...f]; return all.length ? '?' + all.join('&') : '' } } }, /^A: a tab change drops pane and place/],
['a whitelist with no length cap', { ...real, va: { ...va, cleanKey: (v) => (/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(String(v ?? '')) ? String(v) : '') } }, /^A: the whitelist lets/],
['unchanged judged by the string, not the values', { ...real, va: { ...va, writeViewKeys: (c, o) => { if (o.owner !== 'app') return va.writeViewKeys(c, o); const s = va.viewSearch(location.search, c); if (s === location.search) return 'unchanged'; return va.commitUrl({ search: s }, { push: o.push }) ? 'written' : 'unchanged' } } }, /^B: the same keys in another order were rewritten/],
['a write when the address is unchanged', { ...real, va: { ...va, writeViewKeys: (c, o) => { const s = va.viewSearch(location.search, c); history[o.push ? 'pushState' : 'replaceState']({ ...(history.state || {}), current: location.pathname + s + location.hash }, '', location.pathname + s + location.hash); return 'written' } } }, /^B: an unchanged address wrote|^C: re-applying the tab after Back WROTE/],
["a 'host' owner that writes history itself", { ...real, va: { ...va, writeViewKeys: (c, o) => va.writeViewKeys(c, { ...o, owner: o.owner === 'host' ? 'app' : o.owner }) } }, /^B: a 'host' owner wrote/],
['a dxm-address that cannot leave a shadow root', { ...real, va: { ...va, writeViewKeys: (c, o) => { if (o.owner !== 'host') return va.writeViewKeys(c, o); const s = va.viewSearch(location.search, c); o.el.dispatchEvent(new CustomEvent(va.ADDRESS_EVENT, { bubbles: true, detail: { keys: va.readViewKeys(s), href: location.pathname + s, push: o.push } })); return 'announced' } } }, /^B: dxm-address cannot leave/],
@ -463,10 +393,7 @@ if (process.argv.includes('--self-test')) {
["pane-wrapper writing a click's selection separately", { ...real, pw: await pwVariant('if (write && known(paneId)) claimPane(paneId, push, changes)', 'if (write && known(paneId)) { claimPane(paneId, push); if (Object.keys(changes).length) claimView(changes, { owner: address.owner(), el: host, push }) }') }, /^F: a click's pane and its selection are not ONE push/],
['pane-wrapper pushing where a scroll settles', { ...real, pw: await pwVariant('claimPane(current(), false)', 'claimPane(current(), true)') }, /^F: where the reader's scroll settles/],
['pane-wrapper writing on Back (the pane it is leaving)', { ...real, pw: await pwVariant("show(known(pane) ? pane : column[0].id, 'smooth')", "(claimPane(current(), true), show(known(pane) ? pane : column[0].id, 'smooth'))") }, /^F: Back did not show the address's pane, or it WROTE/],
['pane-wrapper opening a link at build time only (before the column has a layout)', { ...real, pw: await pwVariant("let pending = known(linked) && linked !== column[0].id ? linked : ''", "let pending = ''; if (known(linked)) show(linked, 'instant')") }, /^F: a FRESH LOAD with \?pane=dashboard did not open/],
['pane-wrapper whose settle overwrites a link still waiting to open', { ...real, pw: await pwVariant('if (col.clientHeight && !pending) claimPane(current(), false)', 'if (!pending || true) claimPane(current(), false)') }, /^F: before its layout, the column overwrote the link/],
['pane-wrapper retrying a pending link by rAF only when there is no observer', { ...real, pw: await pwVariant("if (pending && typeof requestAnimationFrame !== 'undefined') {", "if (pending && typeof ResizeObserver === 'undefined' && typeof requestAnimationFrame !== 'undefined') {") }, /^F: with an observer that never fires again/],
['pane-wrapper ignoring a link', { ...real, pw: await pwVariant("let pending = known(linked) && linked !== column[0].id ? linked : ''", "let pending = ''") }, /^F: a link to \?pane=dashboard did not open/],
['pane-wrapper ignoring a link', { ...real, pw: await pwVariant("if (known(asked)) show(asked, 'instant')", 'void asked') }, /^F: a link to \?pane=dashboard did not open/],
['pane-wrapper writing without the opt-in', { ...real, pw: await pwVariant("=> address && claimView({ pane: id === column[0].id ? '' : id, ...changes }, { owner: address.owner(),", "=> claimView({ pane: id === column[0].id ? '' : id, ...changes }, { owner: address ? address.owner() : 'app',") }, /^F: a column WITHOUT the address opt-in/],
['ownAddress that lets a nested element switch the tab', { ...real, va: { ...va, ownAddress: (el, g) => { const h = (e) => { const o = g(); if (o) { e.stopPropagation(); va.writeViewKeys(e.detail.changes, { owner: o, el, push: e.detail.push }) } }; el.addEventListener(va.VIEW_EVENT, h); return () => el.removeEventListener(va.VIEW_EVENT, h) } } }, /^E: a nested announcement switched the OWNER's tab/],
['pane-wrapper not claiming its pane when shown again', { ...real, pw: await pwVariant("if (typeof ResizeObserver !== 'undefined') {", 'if (false) {') }, /^F: a kept-alive column shown again/],
@ -475,12 +402,6 @@ if (process.argv.includes('--self-test')) {
['region-dashboards not claiming its place when shown again', { ...real, src: { ...src, dash: src.dash.replace('else if (!place) claimPlace(false)', 'else if (!place) void 0') } }, /^G: a kept-alive region-dashboards shown again/],
['regions-mfe opening the dashboard without its place', { ...real, src: { ...src, mfe: src.mfe.replace("go('dashboard', { place: detail.countrySlug || detail.regionSlug || '' })", "go('dashboard')") } }, /^G: regions-mfe opens the dashboard without its place/],
['regions-mfe without the address opt-in', { ...real, src: { ...src, mfe: src.mfe.replace('{ host: stage.value, address: { owner } }', '{ host: stage.value }') } }, /^G: regions-mfe does not opt its column in/],
['a composite that opts in without ownAddress', { ...real, src: { ...src, opted: { ...src.opted, 'plugin-designs/x/x-mfe.vue': "column = buildPaneColumn(spec.value, { host: stage.value, address: { owner } })" } } }, /^G: plugin-designs\/x\/x-mfe\.vue opts its column in but does not record/],
['no composite opted in at all', { ...real, src: { ...src, opted: {} } }, /^G: no composite opts its pane column/],
['Plugin Studio without ownAddress', { ...real, src: { ...src, shells: { ...src.shells, 'plugin-studio/plugin-studio.vue': src.shells['plugin-studio/plugin-studio.vue'].replace('this._offOwn = ownAddress(this.$el, () => urlOwnerOf(this.cfg))', 'this._offOwn = null') } } }, /^G: the shell plugin-studio\/plugin-studio\.vue does not record/],
['Plugin Studio releasing ownAddress after its hot-update return', { ...real, src: { ...src, shells: { ...src.shells, 'plugin-studio/plugin-studio.vue': src.shells['plugin-studio/plugin-studio.vue'].replace(' if (this._offOwn) { this._offOwn(); this._offOwn = null }\n if (import.meta.hot) return', ' if (import.meta.hot) return\n if (this._offOwn) { this._offOwn(); this._offOwn = null }') } } }, /^G: the shell plugin-studio\/plugin-studio\.vue does not release/],
['the Research Gallery without ownAddress', { ...real, src: { ...src, shells: { ...src.shells, 'doxa-maps-spa/doxa-maps-spa.vue': src.shells['doxa-maps-spa/doxa-maps-spa.vue'].replace('this._offOwn = ownAddress(', 'this._offOwn = (') } } }, /^G: the shell doxa-maps-spa/],
['Posterverse never releasing ownAddress', { ...real, src: { ...src, shells: { ...src.shells, 'posterverse/posterverse.vue': src.shells['posterverse/posterverse.vue'].replace('if (this._offOwn) { this._offOwn(); this._offOwn = null }', 'void 0') } } }, /^G: the shell posterverse\/posterverse\.vue does not release/],
['region-dashboards not re-applying ?place= on Back', { ...real, src: { ...src, dash: src.dash.replace('onAddressChange(onAddress)', 'null') } }, /^G: region-dashboards does not re-apply/],
]
for (const [name, inp, rx] of poisons) {

View file

@ -145,18 +145,6 @@ export function galleryModel(reg) {
}
}
// ── ONE PART OF A LEAF URL ───────────────────────────────────────────────────────────────────
// /__leaf/<bundle>/<profile>[/<instance>] parts are written INTO the leaf page: its <title> and the
// element's profile-config='…' attribute. Refusing only `..` and `/` let a quote or a tag through, so a
// crafted link broke out of the attribute on a publicly reachable dev server. Every bundle folder,
// profile id and instance id in the tree has this shape (the address keys' own pattern,
// library/composables/useViewAddress.js), so anything else is refused before it reaches either sink;
// it also rules out `..` and `/`. check:dev-leaf-escape proves both directions.
export const LEAF_PART = /^[A-Za-z0-9][\w-]{0,63}$/
// For an HTML text node or a quoted attribute value.
export const escHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c])
// ── THE PRODUCTION EMBED URL, ANSWERED BY THE DEV SERVER ─────────────────────────────────────
// The manifest advertises, and a person copies, tree-relative embed URLs of the shape
// `<bundle>/<profile>/index.html` and `<bundle>/<profile>/<instance>/index.html` — served in
@ -211,8 +199,8 @@ export function leafRewrite(path, treeName = TREE_NAME) {
// urlOwner 'app' (library/composables/useViewAddress.js) for a dev page that is OURS, the rule generate-tree bakes
// into the built leaf: its shell writes ?tab=, so a clicked tab survives a reload and a copied link opens it. A
// FRAMED page drops it: a partner page may iframe a leaf (the iframe embed), and a history push from an iframe
// lands in the top page's Back stack. (Plugin Studio mounts its rows directly; its panes are unowned by paneConfig.) Write the element unowned and put this classic script right after it; it runs
// FRAMED page drops it: Plugin Studio iframes its map rows from /__leaf/, and a history push from an iframe lands
// in the top page's Back stack. Write the element unowned and put this classic script right after it; it runs
// before the deferred module script upgrades the element. `<` is escaped: profile/instance come from the URL.
export function ownIfTopScript(cfg = {}) {
const owned = JSON.stringify(JSON.stringify({ ...cfg, urlOwner: 'app' })).replace(/</g, '\\u003c')
@ -359,8 +347,8 @@ export function autoSpaDevPlugin(token = '') {
try { return decodeURIComponent(s) } catch { return '' }
})
const [bundle, profile, instance] = parts
// These land in a filesystem-ish URL and in the page's HTML below: LEAF_PART shapes only.
if (!bundle || !profile || parts.some(s => !LEAF_PART.test(s))) {
// Path traversal guard: these land in a filesystem-ish URL below.
if (!bundle || !profile || parts.some(s => !s || s.includes('..') || s.includes('/'))) {
res.statusCode = 400; return res.end('bad leaf path')
}
let reg
@ -399,7 +387,7 @@ export function autoSpaDevPlugin(token = '') {
res.setHeader('Cache-Control', 'no-cache')
return res.end([
'<!DOCTYPE html>',
`<html lang="en"><head><meta charset="UTF-8" /><title>${escHtml(bundle)} · ${escHtml(profile)}</title>`,
`<html lang="en"><head><meta charset="UTF-8" /><title>${bundle} · ${profile}</title>`,
'<meta name="viewport" content="width=device-width, initial-scale=1.0" />',
'<link rel="icon" href="data:,">',
leafTok ? `<script>window.MAP_TOKEN=${JSON.stringify(leafTok)};</script>` : '',

View file

@ -14,11 +14,6 @@
RIGHT of left-hand controls. The side is measured, so a right-to-left page that
mirrors the toolbar mirrors the menu as well.
• Width: as wide as fits between the toolbar and the opposite edge, capped at `width`.
• A STANDALONE anchor (ShareButton in a page header, no map toolbar: `standalone` on the
provided anchor) opens BELOW its button instead, toward the side with room, inside the
VIEWPORT, at `width` (the maps' readable 340) or the viewport minus its edges, whichever is
less. Where it lives and where it goes are resolved when it OPENS, and its coordinates are
converted into the block that really contains it (see placeBeside).
• One z tier for menus (--z-menu, default 3000); only one menu is open per map at a
time; closes on Escape or on a tap on the bare map canvas (never on legend/search).
@ -100,17 +95,23 @@ const AREA = '.dsm-map-area, .rm-map-area, .dm-map-area, [data-map-area]'
function place() {
const tb = toolbar?.el?.value, area = host.value
if (!tb || !area) { style.value = {}; return }
if (toolbar.standalone) return placeBeside(tb)
const ar = area.getBoundingClientRect(), tr = tb.getBoundingClientRect()
const onRight = (tr.left + tr.right) / 2 > (ar.left + ar.right) / 2
const gap = 8, edge = 12
const offset = onRight ? (ar.right - tr.left + gap) : (tr.right - ar.left + gap)
const width = Math.max(200, Math.min(props.width, ar.width - offset - edge))
const top = Math.max(0, tr.top - ar.top)
// A menu never outgrows the MAP's walls. (A header button is not placed here: placeBeside bounds it by the
// viewport, so it opens at natural height; measured on upg-100-mfe, a header band as the wall gave a 291px
// menu a 69px scrolling body, BUG-SHARE-MENU-HAS-A-SCROLLBAR-SHOULD-BE-FULLY-EXPANDED.)
let maxHeight = Math.max(120, ar.height - top - edge)
// "Fit inside the map" is the rule ONLY for a real map area. When ShareButton mounted in a
// page HEADER, its fallback self-tagged that header band as the area (data-map-area=
// "share-fallback", see ShareButton.bindAnchor) -- and this line then dutifully clamped the
// menu INSIDE a ~56px band: measured on upg-100-mfe, a 291px share menu rendered with a 69px
// scrolling body (BUG-SHARE-MENU-HAS-A-SCROLLBAR-SHOULD-BE-FULLY-EXPANDED). A menu must never
// outgrow a MAP's walls, but a header band is not a wall -- nothing clips below it -- so the
// fallback area bounds against the VIEWPORT instead and the menu opens at natural height.
const isFallbackArea = area.getAttribute && area.getAttribute('data-map-area') === 'share-fallback'
let maxHeight = isFallbackArea
? Math.max(120, (window.innerHeight || 900) - tr.top - edge) // header host: the page below is available
: Math.max(120, ar.height - top - edge) // map host: fit inside the map
if (props.matchToolbar) maxHeight = Math.max(120, Math.min(maxHeight, tr.height))
if (props.maxHeight) maxHeight = Math.min(maxHeight, props.maxHeight)
style.value = {
@ -122,45 +123,6 @@ function place() {
}
}
// ★ BESIDE ITS BUTTON, INSIDE THE VIEWPORT (a standalone anchor: a Share button in a page header). Below the
// button, toward the side with room (a button right of centre opens leftward, its end edges aligned; RTL headers
// mirror with no special case, because the side is measured), at the readable width the maps use, clamped inside
// the viewport, as tall as its content up to the viewport's bottom edge.
// ★ THE BLOCK THAT CONTAINS IT IS MEASURED, NOT ASSUMED. The menu is position:absolute, so its coordinates are
// relative to its containing block, which the HOST's markup decides: an unpositioned wrapper hands it to the
// header above. Measuring against one element and positioning against another is what sent the header menu far
// to the left (the UPG-100 list page, 2026-10-03). So the target point is computed in viewport coordinates and converted into
// the containing block's own (its padding edge, scrolled), whatever element that turns out to be.
function placeBeside(tb) {
const vw = document.documentElement.clientWidth || window.innerWidth || 1024
const vh = window.innerHeight || 800
const r = tb.getBoundingClientRect(), gap = 8, edge = 12
const width = Math.min(props.width, Math.max(160, vw - 2 * edge))
const onRight = (r.left + r.right) / 2 > vw / 2
const x = Math.max(edge, Math.min(onRight ? r.right - width : r.left, vw - edge - width))
const y = r.bottom + gap
let maxHeight = Math.max(120, vh - y - edge)
if (props.maxHeight) maxHeight = Math.min(maxHeight, props.maxHeight)
const o = blockOrigin(el.value)
style.value = {
top: `${Math.round(y - o.y)}px`,
left: `${Math.round(x - o.x)}px`,
width: `${Math.round(width)}px`,
maxHeight: `${Math.round(maxHeight)}px`,
...(props.height ? { height: `${Math.round(Math.min(props.height, maxHeight))}px` } : {}),
}
}
// The viewport position of the origin an absolutely positioned element is placed from: its offsetParent's padding
// edge, scrolled. A static body (or none) means the initial containing block: the document origin.
function blockOrigin(m) {
const cb = m && m.offsetParent
if (!cb || ((cb === document.body || cb === document.documentElement) && getComputedStyle(cb).position === 'static')) {
return { x: -(window.scrollX || 0), y: -(window.scrollY || 0) }
}
const b = cb.getBoundingClientRect()
return { x: b.left + cb.clientLeft - cb.scrollLeft, y: b.top + cb.clientTop - cb.scrollTop }
}
// Only one menu open per map: opening this one closes any other.
const OPEN_EVENT = 'doxa-map-menu-open'
function onOtherOpened(e) { if (props.open && e.detail !== el.value) emit('close') }
@ -177,7 +139,6 @@ const announceClose = () => { try { host.value?.dispatchEvent(new CustomEvent(PA
watch(() => props.open, async (open, was) => {
if (!open) { if (was) announceClose(); return }
if (toolbar?.standalone) host.value = resolveHost() // layout is known now: where the menu lives
await nextTick()
place()
try { host.value?.dispatchEvent(new CustomEvent(OPEN_EVENT, { detail: el.value })) } catch (_) {}
@ -195,27 +156,15 @@ function onClickOutside(e) {
if (path.some(n => n?.classList?.contains?.('mapboxgl-canvas') || n?.classList?.contains?.('maplibregl-canvas'))) emit('close')
}
// Where the menu lives (the Teleport target). A map control: the map area, else the toolbar's offset parent. A
// standalone button: a named area if its host declares one, else its offset parent, kept in the button's own
// (shadow) root so the menu keeps this root's styles; else the root's top element.
function resolveHost() {
const tbEl = toolbar?.el?.value || null
if (!tbEl) return null
if (!toolbar.standalone) return tbEl.closest(AREA) || tbEl.offsetParent || tbEl.parentElement || null
const root = tbEl.getRootNode(), mine = (n) => n && n.getRootNode() === root ? n : null
let top = tbEl; while (top.parentElement) top = top.parentElement
return mine(tbEl.closest(AREA)) || mine(tbEl.offsetParent) || (root === document ? document.body : top)
}
// The "one menu at a time" listener follows the host, which a standalone button re-resolves on every open.
watch(host, (h, was) => { was?.removeEventListener(OPEN_EVENT, onOtherOpened); h?.addEventListener(OPEN_EVENT, onOtherOpened) })
onMounted(() => {
// ★ NEVER null. A named area is still preferred -- it is the MAP's walls, and a menu must not outgrow
// them -- but when a plugin has not tagged one, the toolbar's own offsetParent is the containing block
// this menu is absolutely positioned against, so placing against it is geometrically correct by
// definition rather than by convention. A plugin that adopts a class or data-map-area later simply
// gets the better anchor; nothing has to be registered anywhere for the menu to be on screen.
host.value = resolveHost()
const tbEl = toolbar?.el?.value || null
host.value = tbEl?.closest(AREA) || tbEl?.offsetParent || tbEl?.parentElement || null
host.value?.addEventListener(OPEN_EVENT, onOtherOpened)
window.addEventListener('resize', onResize)
document.addEventListener('click', onClickOutside, true)
document.addEventListener('keydown', onKeyDown)

View file

@ -2,13 +2,10 @@
ShareButton.vue — Non-technical share modal for the map toolbar.
Redesigned to match consumer-grade UX from YouTube, Spotify, Canva, and
Google Maps. Two tabs, share FIRST (shareable pages P3):
Google Maps. Two tabs:
1. "Share this page" — the link to exactly what the reader is looking at (the page's own
address, with its tab, where the page owns its address) + one-click copy. It is what a
person who presses Share expects: something to send to someone.
2. "Embed" — for putting the map on ANOTHER website: what embedding is and why it helps, the
multiplication steps, then the code with size presets.
1. "Share Link" — clean URL display + one-click copy + social share icons
2. "Add to Your Website" — friendly embed guidance with size presets
The word "iframe" never appears in any user-visible text. The embed tab
leads with a plain-English description, then shows the code below.
@ -33,9 +30,7 @@ import { tStatic } from '@map/i18n/index.js'
const T = (key, en) => tStatic(key) || en
import { useI18n } from 'vue-i18n'
import { useShadowStyles } from '../../composables/useShadowStyles.js'
import { resolveShareUrl, resolvePublicBase, resolveEmbedBase } from '../../utils/embedBase.js'
import { publicLeafUrl, publicScriptCode } from '../../utils/publicEmbed.js'
import { urlOwnerOf } from '../../composables/useViewAddress.js'
import { resolveShareUrl } from '../../utils/embedBase.js'
import MapControlButton from './MapControlButton.vue'
import MapControlMenu from './MapControlMenu.vue'
@ -56,7 +51,7 @@ useShadowStyles(`
/* The ring itself rotates, so the colours travel around the button in every browser. */
@keyframes share-ring-turn{to{transform:rotate(360deg);}}
@media(prefers-reduced-motion:reduce){.share-wrap::before{animation:none;}}
.share-steps{margin:0 0 12px;padding:0 0 0 15px;list-style:'- ';font:14px/1.55 system-ui,sans-serif;color:#c9d1d9;}
.share-steps{margin:10px 14px 6px;padding:0 0 0 15px;list-style:'- ';font:14px/1.55 system-ui,sans-serif;color:#c9d1d9;}
.share-steps li{margin:0 0 3px;padding-inline-start:3px;}
.share-steps li:last-child{font-weight:600;color:#92b195;}
.share-pop.light .share-steps{color:#3b463d;}
@ -353,13 +348,10 @@ const rootEl = ref(null)
const isOpen = ref(false)
const popoverEl = ref(null)
const btnEl = ref(null)
const activeTab = ref('page') // 'page' | 'embed'
const pageUrl = ref('') // "Share this page": read when the menu opens (see pageLink)
const activeTab = ref('link') // 'link' | 'embed'
const linkCopied = ref(false)
const embedCopied = ref(false)
const embedSize = ref('medium') // 'small' | 'medium' | 'large'
const embedKind = ref('url') // 'url' | 'iframe' | 'script': the Embed tab's three codes
const scriptState = ref({ loading: false }) // the JavaScript code from the public manifest: { code } | { missing } | { failed }
const sizePresets = {
small: { label: 'Small', height: 400 },
@ -367,9 +359,6 @@ const sizePresets = {
large: { label: 'Large', height: 800 }
}
// The size names, each key written out so the catalogue gate sees it.
const sizeLabel = (k) => ({ small: T('share.size_small', 'Small'), medium: T('share.size_medium', 'Medium'), large: T('share.size_large', 'Large') })[k] || k
/* ---- computed ---- */
/* The standalone embed page this map is shared/iframed as — resolved by
utils/embedBase.js (its header documents the full ladder). In short:
@ -393,76 +382,18 @@ const shareUrl = computed(() => resolveShareUrl({
profileConfig: unref(profileConfig) || null,
}))
/* ★ EMBED THIS PLUGIN, FROM THE PUBLIC SITE (shareable pages P3b). The Embed tab's three codes are for the plugin
whose Share was pressed (its own page; its own bundle + element), even nested in Plugin Studio, Deep Research or
Posterverse, and they point at the tree's PUBLIC address (resolvePublicBase: the build's shareBaseUrl), never a dev
server or the page's own host. With no public address configured, the tree this bundle loaded from stands in. */
const embedBase = computed(() => resolvePublicBase(unref(profileConfig) || null) || resolveEmbedBase(unref(profileConfig) || null))
const embedPathOf = () => props.embedPath || (unref(profileConfig) || {}).shareEmbedPath || ''
// URL: the plugin's own generated page, and nothing else: never the app page it is shown in. With no base (the dev
// server, where modules are not bundled) or no tree-relative path, it is '' and the tab shows the plain note.
const leafUrl = computed(() => publicLeafUrl(embedBase.value, embedPathOf()))
const embedSnippet = computed(() => {
const h = sizePresets[embedSize.value].height
return '<iframe src="' + leafUrl.value + '" width="100%" height="' + h + '" style="border:none; border-radius:12px;" allowfullscreen></iframe>'
return '<iframe src="' + shareUrl.value + '" width="100%" height="' + h + '" style="border:none; border-radius:12px;" allowfullscreen></iframe>'
})
// JavaScript: the plugin's embed.script from the public manifest, read once the Embed tab opens. "missing" means the
// public site does not carry this plugin yet, so the tab shows a plain note instead of codes that would 404.
function loadScriptCode() {
if (scriptState.value.code || scriptState.value.loading) return
scriptState.value = { loading: true }
publicScriptCode(embedBase.value, embedPathOf()).then((r) => { scriptState.value = r })
}
const embedText = computed(() => embedKind.value === 'url' ? leafUrl.value
: embedKind.value === 'iframe' ? embedSnippet.value : (scriptState.value.code || ''))
const EMBED_KINDS = ['url', 'iframe', 'script']
const kindLabel = (k) => ({ url: T('share.kind_url', 'URL'), iframe: T('share.kind_iframe', 'Iframe'), script: T('share.kind_script', 'JavaScript') })[k]
// One plain sentence per code, for a reader who has never embedded anything.
const kindHint = (k) => ({
url: T('share.url_hint', 'A link to open the map on its own page.'),
iframe: T('share.iframe_hint', 'Put the map in your page, in a frame.'),
script: T('share.script_hint', 'Load the map straight into your page, with no frame.'),
})[k]
/* ★ SHARE THIS PAGE. Where the page OWNS its address (useViewAddress: urlOwner 'app' on our own pages,
'host' on DX Life), the address bar already names the exact view (the tab, and the filters), so the
link is the page's own URL, read when the menu opens and again at copy time, since the reader can
switch tabs in between. The owner is this map's own profile-config, or the nearest element above it,
across shadow roots, whose profile-config names one: a map nested in the gallery is told nothing, and
the gallery is the page. On a partner's page (no owner) the address is the partner's, so the link is
this map's own page, as before. */
const cfgOf = (c) => { if (typeof c !== 'string') return c || null; try { return JSON.parse(c) } catch { return null } }
function pageOwner() {
if (urlOwnerOf(cfgOf(unref(profileConfig)))) return true
for (let n = rootEl.value; n; ) {
n = n.parentNode || n.host
if (n && n.nodeType === 1 && urlOwnerOf(cfgOf(n.profileConfig ?? n.getAttribute('profile-config')))) return true
}
return false
}
// A leaf page reads a token from ?tk= and leaves it in its address, so the page link drops it: a reader must
// never send a credential by pressing Share. The URL is rewritten only when tk is there, so every other link is
// the address exactly as it stands.
function pageLink() {
try {
if (pageOwner() && window.location.href) {
const u = new URL(window.location.href)
if (u.searchParams.has('tk')) { u.searchParams.delete('tk'); return u.href }
return window.location.href
}
} catch { /* no window */ }
return shareUrl.value
}
/* ---- actions ---- */
function toggle() {
isOpen.value = !isOpen.value
if (isOpen.value) {
activeTab.value = 'page'
pageUrl.value = pageLink()
activeTab.value = 'link'
linkCopied.value = false
embedCopied.value = false
embedKind.value = 'url'
}
}
@ -494,15 +425,14 @@ async function copyText(text) {
}
async function copyLink() {
pageUrl.value = pageLink()
if (await copyText(pageUrl.value)) {
if (await copyText(shareUrl.value)) {
linkCopied.value = true
setTimeout(() => { linkCopied.value = false }, 2000)
}
}
async function copyEmbed() {
if (embedText.value && await copyText(embedText.value)) {
if (await copyText(embedSnippet.value)) {
embedCopied.value = true
setTimeout(() => { embedCopied.value = false }, 2000)
}
@ -547,24 +477,39 @@ async function copyEmbed() {
const providedToolbar = inject('mapToolbar', null)
const selfAnchor = ref(null)
// Re-provide UNCONDITIONALLY so the value is stable for the child's inject(); `el` stays null when
// a real toolbar was found, and MapControlMenu keeps using that toolbar instead. `standalone` tells
// MapControlMenu this anchor is ONE button outside any map (a page header), placed beside the button
// inside the viewport, at open time (MapControlMenu.placeBeside).
provide('mapToolbar', providedToolbar || { el: selfAnchor, standalone: true })
// a real toolbar was found, and MapControlMenu keeps using that toolbar instead.
provide('mapToolbar', providedToolbar || { el: selfAnchor })
const hasOwnAnchor = !providedToolbar
/*
★ THE ANCHOR IS THE BUTTON, AND NOTHING IS DECIDED AT RENDER TIME (shareable pages P3). This used to tag
`el.offsetParent || el.parentElement` as the menu's area DURING RENDER (a function ref, so it ran before
MapControlMenu latched its area). Before layout offsetParent is null, so the tag fell on the button's
immediate parent: in a header with a narrow, unpositioned wrapper (measured on the UPG-100 list page: .hdr-btns) the menu
measured against that wrapper, clamped to its 200px floor, and was positioned against the header above
it: far to the left, and narrow (reported 2026-10-03). Now the button only names itself;
MapControlMenu resolves where the menu lives and where it goes when it OPENS, with layout known.
★ THIS IS A FUNCTION REF, AND onMounted WOULD NOT HAVE WORKED — the hazard is worth naming,
because the fix silently does NOTHING if it is written the obvious way.
MapControlMenu latches its area ONCE, in its own onMounted:
host.value = toolbar?.el?.value?.closest(AREA) || null
and place() bails forever after if that is null. MapControlMenu is a CHILD of this component, and
Vue runs a child's mounted hook BEFORE its parent's. So an onMounted here would set the anchor
one tick too late: the child would have already read `null`, latched `host = null`, and no later
assignment would revive it — the menu would go on opening at its static position and the fix
would look applied while changing nothing.
A function ref is invoked during RENDER, before any mounted hook on either component, so the
anchor and the area tag are in place by the time the child looks for them.
`data-map-area` is what MapControlMenu.AREA finds via closest(). It is tagged on the OFFSET
PARENT, not on .share-wrap: the wrapper is a 32px button, and an area that small would clamp the
menu to its 200px minimum jammed against the edge. The offset parent is the top bar, which spans
the row, so the menu has the full width to open leftward into.
*/
function bindAnchor(el) {
rootEl.value = el || null
selfAnchor.value = hasOwnAnchor && el ? el : null
if (!hasOwnAnchor || !el) { if (!el) selfAnchor.value = null; return }
const area = el.offsetParent || el.parentElement
// Never re-tag a host that already declares itself an area — that host owns the decision.
if (area && !area.closest('.dsm-map-area, .rm-map-area, .dm-map-area, [data-map-area]')) {
area.setAttribute('data-map-area', 'share-fallback')
}
selfAnchor.value = el
}
/* Placement, stacking, outside-click and Escape live in MapControlMenu (shared by every
@ -572,8 +517,8 @@ function bindAnchor(el) {
</script>
<template>
<!-- :ref is a FUNCTION, deliberately: the anchor must be set before MapControlMenu's mounted hook
reads it (a child mounts before its parent). See bindAnchor() in <script>. -->
<!-- :ref is a FUNCTION, deliberately — it must run during render, before MapControlMenu's
mounted hook latches its area. See bindAnchor() in <script> for why onMounted is too late. -->
<div :ref="bindAnchor" class="share-wrap" :class="{ 'share-wrap--rounded-square': shape === 'rounded-square' }" data-share-root>
<MapControlButton
ref="btnEl"
@ -598,26 +543,29 @@ function bindAnchor(el) {
<!-- =================== Menu (shared MapControlMenu owns placement + chrome) =================== -->
<MapControlMenu :open="isOpen" :title="t('buttons.share')" :is-dark="isDark" @close="close">
<div ref="popoverEl" class="share-pop" :class="{ light: !isDark }">
<!-- Tabs: sharing the PAGE first (what a person pressing Share expects), Embed second -->
<div class="share-tabs" role="tablist">
<!-- The multiplication path — one line each, top of the menu -->
<ul class="share-steps">
<li v-for="(step, i) in tm('buttons.shareSteps')" :key="i">{{ rt(step) }}</li>
</ul>
<!-- Tabs -->
<div class="share-tabs">
<button
class="share-tab" role="tab" data-share-tab="page"
:class="{ active: activeTab === 'page' }" :aria-selected="String(activeTab === 'page')"
@click="activeTab = 'page'"
>{{ T('share.tab_page', 'Share this page') }}</button>
class="share-tab"
:class="{ active: activeTab === 'link' }"
@click="activeTab = 'link'"
>{{ T('share.embed_link', 'Embed link') }}</button>
<button
class="share-tab" role="tab" data-share-tab="embed"
:class="{ active: activeTab === 'embed' }" :aria-selected="String(activeTab === 'embed')"
@click="activeTab = 'embed'; loadScriptCode()"
>{{ T('share.tab_embed', 'Embed') }}</button>
class="share-tab"
:class="{ active: activeTab === 'embed' }"
@click="activeTab = 'embed'"
>{{ T('share.add_to_website', 'Add to Your Website') }}</button>
</div>
<!-- ===== Tab 1: Share this page ===== -->
<div v-if="activeTab === 'page'" class="share-body">
<div class="share-embed-desc">{{ T('share.page_hint', 'A link that opens this exact page for whoever you send it to.') }}</div>
<!-- ===== Tab 1: Share Link ===== -->
<div v-if="activeTab === 'link'" class="share-body">
<!-- URL display -->
<div class="share-url-box">
<div class="share-url-text" data-share-url>{{ pageUrl }}</div>
<div class="share-url-text">{{ shareUrl }}</div>
</div>
<!-- Copy Link primary CTA -->
@ -638,72 +586,50 @@ function bindAnchor(el) {
stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
{{ linkCopied ? T('share.copied', 'Copied!') : T('share.copy_link', 'Copy link') }}
{{ linkCopied ? 'Copied!' : 'Copy Link' }}
</button>
</div>
<!-- ===== Tab 2: Embed (for another website): THIS plugin, from the public site ===== -->
<!-- ===== Tab 2: Add to Your Website ===== -->
<div v-if="activeTab === 'embed'" class="share-body">
<!-- What embedding is and why it helps, in plain words, before any code -->
<!-- Friendly description -->
<div class="share-embed-desc">
{{ T('share.embed_what', 'Embedding puts this live map inside your own website, so your visitors can explore it and pray without leaving your site. It stays up to date by itself.') }}
{{ T('share.embed_paste_hint', 'Paste this code into your website builder (WordPress, Squarespace, Wix, or any site that accepts HTML widgets).') }}
</div>
<!-- The multiplication path, one line each -->
<ul class="share-steps">
<li v-for="(step, i) in tm('buttons.shareSteps')" :key="i">{{ rt(step) }}</li>
</ul>
<!-- Not on the public site yet (or no page of its own to point at): one plain note instead of codes that would
404 or that would embed the app around it -->
<div v-if="scriptState.missing || !leafUrl" class="share-embed-desc" data-embed-note>
{{ T('share.not_public', "This map isn't on the public site yet. Its embed codes will appear here once it's released.") }}
</div>
<template v-else>
<!-- Three ways, one at a time: a link, a frame, or straight into the page -->
<div class="share-size-pills" role="tablist">
<button
v-for="k in EMBED_KINDS" :key="k" type="button" role="tab"
class="share-size-pill" :data-embed-kind="k"
:class="{ active: embedKind === k }" :aria-selected="String(embedKind === k)"
@click="embedKind = k"
>{{ kindLabel(k) }}</button>
</div>
<div class="share-embed-desc">{{ kindHint(embedKind) }}</div>
<!-- Iframe: its height -->
<div v-if="embedKind === 'iframe'" class="share-size-pills">
<button
v-for="(preset, key) in sizePresets"
:key="key"
class="share-size-pill"
:class="{ active: embedSize === key }"
@click="embedSize = key"
>{{ sizeLabel(key) }}</button>
</div>
<!-- JavaScript: read from the public site when this tab opens -->
<div v-if="embedKind === 'script' && !scriptState.code" class="share-embed-desc">
{{ scriptState.failed ? T('share.public_failed', "Couldn't reach the public site just now. Try again in a moment.") : T('share.script_loading', 'Reading the public site…') }}
</div>
<div v-else class="share-code-box" data-embed-code>{{ embedText }}</div>
<!-- One Copy button -->
<!-- Size presets -->
<div class="share-size-pills">
<button
v-if="embedText"
class="share-secondary-btn"
:class="{ copied: embedCopied }"
@click="copyEmbed"
>
<svg v-if="!embedCopied" width="15" height="15" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
<svg v-else width="15" height="15" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
{{ embedCopied ? T('share.copied', 'Copied!') : (embedKind === 'url' ? T('share.copy_link', 'Copy link') : T('share.copy_code', 'Copy code')) }}
</button>
</template>
v-for="(preset, key) in sizePresets"
:key="key"
class="share-size-pill"
:class="{ active: embedSize === key }"
@click="embedSize = key"
>{{ preset.label }}</button>
</div>
<!-- Code box -->
<div class="share-code-box">{{ embedSnippet }}</div>
<!-- Copy Code button -->
<button
class="share-secondary-btn"
:class="{ copied: embedCopied }"
@click="copyEmbed"
>
<svg v-if="!embedCopied" width="15" height="15" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
<svg v-else width="15" height="15" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
{{ embedCopied ? 'Copied!' : 'Copy Code' }}
</button>
</div>
</div>
</MapControlMenu>

View file

@ -36,15 +36,6 @@ const esc = (s) => String(s ?? '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
// A sheet the host GENERATES (opts.sheet) that fails still falls through to the stored one, so a print never comes
// out blank. It must not fall through SILENTLY: a stored sheet with an old header once printed in place of the
// generated one, and nothing said why. The console names the sheet and the reason. An empty result is not a
// failure: a generator returns '' on purpose when it cannot draw that sheet.
const generated = (draw, id, format) => Promise.resolve().then(draw).catch((e) => {
try { console.warn(`[posterPrints] the ${format} sheet for ${id} could not be generated, so the stored sheet prints instead: ${e?.message || e}`); } catch (_) { /* no console */ }
return '';
});
/**
* THE PRINT STANDARD — one call gives any map the poster, the card deck, the prayer list and the
* prayer packet (full or lightweight) for ANY item: a zone, a country, or a custom list of people.
@ -176,7 +167,7 @@ export function usePosterPrints(opts = {}) {
// that cannot generate, and a generation that fails falls through to them rather than to nothing.
if (opts.sheet) {
set(item, kind, 'render', tr('wall.hud.generatingPoster', 'generating the poster'), 35);
const html = await generated(() => opts.sheet(item, 'letter'), id, 'letter');
const html = await Promise.resolve().then(() => opts.sheet(item, 'letter')).catch(() => '');
if (html) {
set(item, kind, 'save', tr('wall.hud.openingPrint', 'opening the print dialog'), 80);
await deliver(html, item, kind);
@ -422,7 +413,7 @@ export function usePosterPrints(opts = {}) {
const mapId = item && 'mapId' in item ? item.mapId : item?.id;
if (!mapSvg && opts.sheet && mapId) {
const target = mapId === item?.id ? item : { ...item, id: mapId };
const html = await generated(() => opts.sheet(target, 'packet'), mapId, 'packet');
const html = await Promise.resolve().then(() => opts.sheet(target, 'packet')).catch(() => '');
if (html) mapSvg = R.liftMap(html, { labelFor: await mapLabelFor() });
}
if (!mapSvg && opts.mapImage) {

View file

@ -98,11 +98,7 @@ export const urlOwnerOf = (cfg) => (cfg && (cfg.urlOwner === 'app' || cfg.urlOwn
export function writeViewKeys(changes, { owner = '', push = true, el = null } = {}) {
if ((owner !== 'app' && owner !== 'host') || !globalThis.location) return 'none'
const search = viewSearch(location.search, changes)
// Unchanged means the same VALUES, not the same string: viewSearch puts the view keys first, so comparing strings
// rewrote a link like ?owner=x&pane=map as ?pane=map&owner=x on every fresh load (measured in a browser), a
// needless replace (and on a host, a needless router.replace).
const now = readViewKeys(location.search), next = readViewKeys(search)
if (VIEW_KEYS.every((k) => now[k] === next[k])) return 'unchanged'
if (search === location.search) return 'unchanged'
if (owner === 'app') return commitUrl({ search }, { push }) ? 'written' : 'unchanged'
return send(el, ADDRESS_EVENT, { keys: readViewKeys(search), href: location.pathname + search + location.hash, push }) ? 'announced' : 'none'
}

View file

@ -20,5 +20,5 @@
"wallHeading": "ملصقات UPG100",
"wallIntroA": "كيف تُطعم جمعًا من خمسة آلاف؟ أجلسهم يسوع جماعاتٍ من خمسين ومن مئة، فصار المستحيل قابلًا للمشاركة (لوقا ٩:١٤). فعلنا المثل مع {n} شعوب غير مبلَّغة: ٢٧ قائمة من نحو خمسين إلى مئة. تُطبع كل قائمة حزمةَ صلاة وملصقًا، ليحمل ٢٧ تلميذًا أو كنيسة أو قائدًا محليًا كلٌّ واحدة — مصلّين لأجلهم ",
"peopleStillLoading": "لا تزال الشعوب قيد التحميل، حاول مرة أخرى بعد لحظة",
"posterDesignTerritory": "ملصق منطقة UPG100"
"posterDesignTerritory": "ملصق منطقة UPG-100"
}

View file

@ -20,5 +20,5 @@
"wallHeading": "UPG100-Poster",
"wallIntroA": "Wie würden Sie eine Menge von fünftausend speisen? Jesus ließ sie in Gruppen zu fünfzig und hundert lagern, und das Unmögliche wurde teilbar (Lukas 9,14). Dasselbe haben wir mit den {n} unerreichten Volksgruppen getan: 27 Listen von etwa fünfzig bis hundert. Jede Liste druckt als Gebetspaket und als Poster, damit 27 Jünger, Gemeinden oder einheimische Leiter je eine tragen können — und für sie beten ",
"peopleStillLoading": "die Volksgruppen werden noch geladen; versuchen Sie es gleich noch einmal",
"posterDesignTerritory": "UPG100-Gebietsplakat"
"posterDesignTerritory": "UPG-100-Gebietsplakat"
}

View file

@ -1,24 +1,5 @@
{
"embed_link": "Embed link",
"add_to_website": "Add to Your Website",
"embed_paste_hint": "Paste this code into your website builder (WordPress, Squarespace, Wix, or any site that accepts HTML widgets).",
"tab_page": "Share this page",
"tab_embed": "Embed",
"page_hint": "A link that opens this exact page for whoever you send it to.",
"embed_what": "Embedding puts this live map inside your own website, so your visitors can explore it and pray without leaving your site. It stays up to date by itself.",
"copy_link": "Copy link",
"copy_code": "Copy code",
"copied": "Copied!",
"size_small": "Small",
"size_medium": "Medium",
"size_large": "Large",
"kind_url": "URL",
"kind_iframe": "Iframe",
"kind_script": "JavaScript",
"url_hint": "A link to open the map on its own page.",
"iframe_hint": "Put the map in your page, in a frame.",
"script_hint": "Load the map straight into your page, with no frame.",
"script_loading": "Reading the public site…",
"not_public": "This map isn't on the public site yet. Its embed codes will appear here once it's released.",
"public_failed": "Couldn't reach the public site just now. Try again in a moment."
"embed_paste_hint": "Paste this code into your website builder (WordPress, Squarespace, Wix, or any site that accepts HTML widgets)."
}

View file

@ -20,5 +20,5 @@
"wallHeading": "UPG100 Posters",
"wallIntroA": "How would you feed a crowd of five thousand? Jesus sat them down in groups of fifty and a hundred, and the impossible became shareable (Luke 9:14). We did the same with the {n} unreached people groups: 27 lists of about fifty to a hundred. Each list prints as a prayer packet and a poster, so 27 disciples, churches or local leaders can each carry one — praying for them ",
"peopleStillLoading": "the people groups are still loading, try again in a moment",
"posterDesignTerritory": "UPG100 zone poster"
"posterDesignTerritory": "UPG-100 territory poster"
}

View file

@ -20,5 +20,5 @@
"wallHeading": "Carteles UPG100",
"wallIntroA": "¿Cómo alimentarías a una multitud de cinco mil? Jesús los sentó en grupos de cincuenta y de cien, y lo imposible se volvió compartible (Lucas 9:14). Hicimos lo mismo con los {n} pueblos no alcanzados: 27 listas de unos cincuenta a cien. Cada lista se imprime como paquete de oración y cartel, para que 27 discípulos, iglesias o líderes locales lleven una cada uno — orando por ellos ",
"peopleStillLoading": "los pueblos aún se están cargando; inténtalo de nuevo en un momento",
"posterDesignTerritory": "Cartel de zona UPG100"
"posterDesignTerritory": "Cartel de territorio UPG-100"
}

View file

@ -20,5 +20,5 @@
"wallHeading": "Affiches UPG100",
"wallIntroA": "Comment nourririez-vous une foule de cinq mille personnes ? Jésus les a fait asseoir par groupes de cinquante et de cent, et l’impossible est devenu partageable (Luc 9.14). Nous avons fait de même avec les {n} peuples non atteints : 27 listes d’environ cinquante à cent. Chaque liste s’imprime en dossier de prière et en affiche, pour que 27 disciples, Églises ou responsables locaux en portent chacun une — en priant pour eux ",
"peopleStillLoading": "les peuples sont encore en cours de chargement ; réessayez dans un instant",
"posterDesignTerritory": "Affiche de zone UPG100"
"posterDesignTerritory": "Affiche de territoire UPG-100"
}

View file

@ -20,5 +20,5 @@
"wallHeading": "UPG100 पोस्टर",
"wallIntroA": "पाँच हज़ार की भीड़ को आप कैसे खिलाते? यीशु ने उन्हें पचास-पचास और सौ-सौ के समूहों में बिठाया, और असंभव बाँटने योग्य हो गया (लूका 9:14). हमने {n} अनपहुँचे जनसमूहों के साथ वही किया: लगभग पचास से सौ तक की 27 सूचियाँ. हर सूची प्रार्थना पैकेट और पोस्टर के रूप में छपती है, ताकि 27 शिष्य, कलीसियाएँ या स्थानीय अगुवे एक-एक ले सकें — उनके लिए प्रार्थना करते हुए ",
"peopleStillLoading": "जनसमूह अभी लोड हो रहे हैं, थोड़ी देर में फिर कोशिश करें",
"posterDesignTerritory": "UPG100 क्षेत्र का पोस्टर"
"posterDesignTerritory": "UPG-100 क्षेत्र पोस्टर"
}

View file

@ -20,5 +20,5 @@
"wallHeading": "Poster UPG100",
"wallIntroA": "Come sfameresti una folla di cinquemila? Gesù li fece sedere a gruppi di cinquanta e di cento, e l’impossibile divenne condivisibile (Luca 9:14). Abbiamo fatto lo stesso con i {n} popoli non raggiunti: 27 liste di circa cinquanta-cento. Ogni lista si stampa come pacchetto di preghiera e manifesto, così 27 discepoli, chiese o responsabili locali ne portano una ciascuno — pregando per loro ",
"peopleStillLoading": "i popoli si stanno ancora caricando; riprova tra un momento",
"posterDesignTerritory": "Poster di zona UPG100"
"posterDesignTerritory": "Manifesto di territorio UPG-100"
}

View file

@ -20,5 +20,5 @@
"wallHeading": "Cartazes UPG100",
"wallIntroA": "Como você alimentaria uma multidão de cinco mil? Jesus os sentou em grupos de cinquenta e de cem, e o impossível tornou-se compartilhável (Lucas 9.14). Fizemos o mesmo com os {n} povos não alcançados: 27 listas de cerca de cinquenta a cem. Cada lista imprime como pacote de oração e cartaz, para que 27 discípulos, igrejas ou líderes locais levem uma cada um — orando por eles ",
"peopleStillLoading": "os povos ainda estão carregando; tente de novo em um instante",
"posterDesignTerritory": "Cartaz de zona UPG100"
"posterDesignTerritory": "Cartaz de território UPG-100"
}

View file

@ -20,5 +20,5 @@
"wallHeading": "Afișe UPG100",
"wallIntroA": "Cum ai hrăni o mulțime de cinci mii? Isus i-a așezat în grupuri de cincizeci și de o sută, iar imposibilul a devenit împărțibil (Luca 9:14). Am făcut la fel cu cele {n} popoare neatinse: 27 de liste de aproximativ cincizeci până la o sută. Fiecare listă se tipărește ca pachet de rugăciune și afiș, ca 27 de ucenici, biserici sau lideri locali să poarte fiecare câte una — rugându-se pentru ei ",
"peopleStillLoading": "popoarele încă se încarcă; încearcă din nou peste o clipă",
"posterDesignTerritory": "Poster de zonă UPG100"
"posterDesignTerritory": "Afiș de teritoriu UPG-100"
}

View file

@ -20,5 +20,5 @@
"wallHeading": "Плакаты UPG100",
"wallIntroA": "Как накормить толпу в пять тысяч человек? Иисус рассадил их группами по пятьдесят и по сто, и невозможное стало посильным (Лк. 9:14). Мы поступили так же с {n} неохваченными народами: 27 списков примерно по пятьдесят-сто. Каждый список печатается как молитвенный пакет и плакат, чтобы 27 учеников, церквей или местных служителей взяли по одному — молясь о них ",
"peopleStillLoading": "народы ещё загружаются; попробуйте ещё раз через мгновение",
"posterDesignTerritory": "Плакат зоны UPG100"
"posterDesignTerritory": "Плакат территории UPG-100"
}

View file

@ -20,5 +20,5 @@
"wallHeading": "UPG100 海报",
"wallIntroA": "你会怎样喂饱五千人的群众?耶稣让他们一排五十、一排一百地坐下,不可能的事就变得可以分担了(路加福音 9:14)。我们对 {n} 个未得之民做了同样的事:27 份名单,每份约五十到一百。每份名单都能印成祷告手册和海报,让 27 位门徒、教会或当地领袖各领一份 — 为他们祷告 ",
"peopleStillLoading": "族群仍在加载中,请稍后再试",
"posterDesignTerritory": "UPG100 区域海报(单张)"
"posterDesignTerritory": "UPG-100 区域海报"
}

View file

@ -188,32 +188,13 @@ export function buildPaneColumn(spec, { host, mountPane, address }) {
const show = (id, behavior) => { const t = paneEl(id); if (t) col.scrollTo({ top: t.offsetTop, behavior }) }
const offs = []
if (address) {
// A link opens its pane. Reading writes nothing, and the default needs no write. ★ NOT AT ONCE. MEASURED in a
// browser (a fresh load with ?pane=<second pane>, on Country Posters and Regions): when the column is built it
// has no layout yet, so the pane's offsetTop is 0 and the column stayed on its first pane. The link waits until
// the column has a height and the pane a top, then shows it, once. 'instant': 'auto' would take the column's CSS
// smooth scrolling.
const linked = readViewKeys(typeof location !== 'undefined' ? location.search : '').pane
let pending = known(linked) && linked !== column[0].id ? linked : ''
const openLink = () => {
if (!pending || !col.clientHeight || !(paneEl(pending)?.offsetTop > 0)) return
show(pending, 'instant')
pending = ''
}
openLink()
// Retried by the ResizeObserver below, by every settled scroll, and by a bounded rAF loop that runs even WITH an
// observer: the observer fires only when the COLUMN resizes, and a pane's height can arrive later (styles adopted
// late in a shadow root).
if (pending && typeof requestAnimationFrame !== 'undefined') {
let n = 0
const tick = () => { openLink(); if (pending && ++n < 120) requestAnimationFrame(tick) }
requestAnimationFrame(tick)
}
// A link opens its pane at once. Reading writes nothing, and the default needs no write.
const asked = readViewKeys(typeof location !== 'undefined' ? location.search : '').pane
if (known(asked)) show(asked, 'instant') // 'auto' would take the column's CSS smooth scrolling
// Where the reader's own scroll settles is a REPLACE, so a copied link opens what is on screen without a Back
// step per scroll. A column with no height (a hidden tab) has no "where", and a link still waiting to open must
// not be overwritten, so neither writes.
// step per scroll. A column with no height (a hidden tab) has no "where", so it writes nothing.
let t = null
const settle = () => { if (t) clearTimeout(t); t = setTimeout(() => { openLink(); if (col.clientHeight && !pending) claimPane(current(), false) }, 120) }
const settle = () => { if (t) clearTimeout(t); t = setTimeout(() => { if (col.clientHeight) claimPane(current(), false) }, 120) }
col.addEventListener('scroll', settle, { passive: true })
offs.push(() => { if (t) clearTimeout(t); col.removeEventListener('scroll', settle) })
offs.push(onAddressChange(({ pane }) => show(known(pane) ? pane : column[0].id, 'smooth')))
@ -223,7 +204,6 @@ export function buildPaneColumn(spec, { host, mountPane, address }) {
if (typeof ResizeObserver !== 'undefined') {
let h = null
const ro = new ResizeObserver(() => {
openLink()
const was = h
h = col.clientHeight
if (was !== 0 || !h) return

View file

@ -376,11 +376,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
.pray-head{position:absolute;inset-inline-end:${M}px;display:grid;grid-template-columns:repeat(${PRAY_COLS.length},${PRAY_CELL}px);
height:${PRAY_HEAD_H - 6}px;align-items:end;font:700 15px/1.2 Georgia,serif;color:#6f6a5c;text-align:center}
.pray-head span{text-wrap:balance;padding:0 0 5px;border-bottom:2px solid #3b463d}
/* Page 1 of the Full detail list: the explanation, then the head row and the rows right under it (listPages). */
.fd-first{position:absolute;left:${M}px;top:${M + HEAD}px;width:${CW.toFixed(0)}px}
.fd-top{max-height:${FD_TOP_H}px;overflow:hidden}
.fd-rows{position:relative}
.fd-rows .pray-head{inset-inline-end:0}
.fd-top{position:absolute;left:${M}px;top:${M + HEAD}px;width:${CW.toFixed(0)}px;height:${FD_TOP_H}px;overflow:hidden}
.hd-r{position:absolute;right:${M}px;top:54px;font:400 19px Georgia,serif;color:#8c8170}
/* ⚠ box-sizing:border-box IS LOAD-BEARING HERE, NOT HOUSEKEEPING. Without it the row's height
is the CONTENT box and the padding + border are added OUTSIDE it, so the row renders taller
@ -1195,14 +1191,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
PRAY_COLS.map((key) => `<span>${esc(t(key))}</span>`).join('')}</div>`;
for (let start = 0, page = 0; start < groups.length; page += 1) {
const per = page === 0 ? FD_FIRST : PER_PAGE;
// ★ PAGE 1'S ROWS FOLLOW THE EXPLANATION (2026-10-03). The explanation reserved a fixed FD_TOP_H and the
// rows sat below the reserve, so every language shorter than the tallest left a gap above the head row
// (0.36-1.30 in, measured in all 11). Now the explanation (at most FD_TOP_H) and the rows share one
// column (.fd-first), and the rows are placed inside .fd-rows, right under it. The page still holds
// FD_FIRST rows, sized for the full reserve, so a shorter explanation leaves the room at the page's foot.
// The rows stay absolutely placed (see below), only their origin moves: no row can reflow to a page.
const rowsTop = page === 0 ? PRAY_HEAD_H : M + HEAD;
const left = page === 0 ? 0 : M;
const rowsTop = page === 0 ? FD_ROWS_TOP : M + HEAD;
const slice = groups.slice(start, start + per);
// ★ ONE FULL-WIDTH ROW PER GROUP. The column index is gone: `top` advances by the row, and the
// three cells sit inside it. Absolute positioning is kept rather than switching to flow layout
@ -1219,7 +1208,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
const img = src
? `<img src="${esc(src)}" alt="">`
: `<div class="nophoto">${esc(t('no_photo'))}</div>`;
return `<div class="card" style="left:${left}px;top:${top.toFixed(0)}px;
return `<div class="card" style="left:${M}px;top:${top.toFixed(0)}px;
width:${CW.toFixed(0)}px;height:${CH.toFixed(0)}px">${img}
<div class="d-cell">
<div class="nm">${i + 1}. ${esc(g.name)}</div>
@ -1228,17 +1217,15 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
<div class="pray-cols">${PRAY_COLS.map(() => '<span class="pray-col"><i class="pray-box"></i></span>').join('')}</div>
</div>`;
}).join('');
const rowsH = PRAY_HEAD_H + slice.length * (CH + GUT) - GUT;
const body = page === 0
? `<div class="fd-first"><div class="fd-top">${explainTop()}${explainBand(groups.length,
const first = page === 0
? `<div class="fd-top">${explainTop()}${explainBand(groups.length,
{ w: PRAY_COLS.length * PRAY_CELL, cols: `repeat(${PRAY_COLS.length},${PRAY_CELL}px)`, gap: 0, cell: 'xb-cell xb-cell-fd' })}</div>`
+ `<div class="fd-rows" style="height:${rowsH.toFixed(0)}px">${prayHead(0)}${cards}</div></div>`
: `${prayHead(rowsTop - PRAY_HEAD_H)}${cards}`;
: '';
pages.push(sheet(`
<div class="hd">${esc(zone.label || zone.id)}</div>
<div class="hd-r">${esc(fill(t('list_header'),
{ from: num(start + 1), to: num(Math.min(start + per, groups.length)), count: num(groups.length) }))}</div>
${body}`));
${first}${prayHead(rowsTop - PRAY_HEAD_H)}${cards}`));
start += per;
}
return pages;
@ -1397,7 +1384,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
// its own width, with per-script advances measured on this page in the packet's fonts and rounded UP; a
// tier is taken only when the estimate fits with PT_SAFE to spare. check:packet-render measures the real
// page in all 11 locales behind it (pointsOver).
const PT_MIN = { head: 22, sub: 19, text: 18, refs: 16, gap: 3, passage: 16, th: 19, thb: 20, scr: 18 };
const PT_MIN = { head: 22, sub: 19, text: 18.5, refs: 16, gap: 5, passage: 16, th: 19, thb: 20, scr: 18 };
const PT_MAX = { head: 25, sub: 21, text: 22.5, refs: 18.5, gap: 18, passage: 20, th: 21, thb: 22, scr: 19 };
const PT_TOP = 82, PT_TIERS = 10, PT_SAFE = 0.97;
const PT_BODY_H = H - (M + PT_TOP) - 80; // the .points-body box: 1719px
@ -1645,18 +1632,13 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
+ `<rect x="${(cx - arm / 2).toFixed(1)}" y="${(cy - h / 2 + h * 0.24).toFixed(1)}" width="${arm.toFixed(1)}" height="${bar.toFixed(1)}" rx="1.5"/></g>`;
}
function setsPage(zone, sets, listSize) {
function setsPage(zone, sets) {
// '' WITHOUT DATA, exactly like funnelPage and christianPage — a page that cannot state true
// figures must not appear at all. This is what keeps FULL valid before a host supplies sets.
if (!sets || (!Array.isArray(sets.bounded) && !Array.isArray(sets.centered))) return '';
const bounded = Array.isArray(sets.bounded) ? sets.bounded : [];
const centered = Array.isArray(sets.centered) ? sets.centered : [];
if (!bounded.length && !centered.length) return '';
// ★ EVERY GROUP OF THE LIST IS COUNTED (2026-10-03). A group with no record in a figure's data (no
// near-neighbour data, say) is in none of its figures, so a caption used to add up to less than the
// list: Laos printed "88 placed · 0 at the centre · 6 unplaced", 94 of its 95. The groups missing from
// a figure's data are now named in its caption ("1 without data"), so its parts add up to the list.
const noData = (n) => (Number.isFinite(listSize) && listSize > n ? ` · ${fill(t('sets_no_data'), { n: num(listSize - n) })}` : '');
const panels = [];
let legend = '';
@ -1679,7 +1661,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
${cross(L.cx, L.cy, 44)}
</svg>
<p class="set-cap">${esc(fill(t('sets_bounded_tally'),
{ inside: num(T.inside), outside: num(T.outside), unknown: num(T.unknown) }) + noData(bounded.length))}</p>
{ inside: num(T.inside), outside: num(T.outside), unknown: num(T.unknown) }))}</p>
</div>` });
}
@ -1731,7 +1713,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
${bands.join('')}${dots}${nums.join('')}${cross(0, 0, 40)}
</svg>
<p class="set-cap">${esc(fill(t('sets_centered_tally'),
{ placed: num(bf.placed.length), centre: num(T.centre), unplaced: num(T.unplaced + bf.unplaced.length) }) + noData(centered.length))}</p>
{ placed: num(bf.placed.length), centre: num(T.centre), unplaced: num(T.unplaced + bf.unplaced.length) }))}</p>
</div>` });
const rows = ringLegend(T);
// The legend first says what a ring IS, in plain words, then lists the rings that hold someone.
@ -1898,7 +1880,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
// packet lists the SAME groups as a worksheet (LIST_STYLE); the full packet as profiles.
...(LIST_STYLE[variant] ? LIST_STYLE[variant](zone, groups, photos) : listPages(zone, groups, photos)),
// BACK MATTER: after the profiles (brief A1). Full-detail only.
keep('sets') && setsPage(zone, sets, Array.isArray(groups) ? groups.length : NaN),
keep('sets') && setsPage(zone, sets),
keep('wheel') && wheelPage(),
keep('points') && pointsPage(),
].filter(Boolean);

View file

@ -2,14 +2,14 @@
// The sha384 of library/packet/poster-render.js. The browser fetches the served copy of that file
// (<assets>/lib/poster-render.js), hashes it, and runs it only when it matches this pin: code loaded at
// print time is integrity-pinned like every embed, because it executes on partner pages.
export const POSTER_RENDER_SHA384 = 'sha384-BNFUrsdCCaEetmp5cSBzDklAUsB5XBEefQEcCd3+dNNDghxRuZNytNsVfl5mBOeP'
export const POSTER_RENDER_SHA384 = 'sha384-TvWYyN1nK44Yjuw4yEIyrJmJG2zcAOTIBiTryFGtNHOI67cyWzEXRxHOmsRZagq1'
// The sha384 of library/scripts/posters/data/ne-places.json, served at <assets>/boundaries/ne-places.json.
// Data, not code, but pinned the same way: a sheet uses the places only when they are the pinned ones.
export const NE_PLACES_SHA384 = 'sha384-Qtz2q8mcnA9Ei8Kwa2MRMyX95hSbSnUXjXdtsy8rHTX++mgSCih4LO4Rdrb2MIQy'
// The sha384 of the classic-script copy (<assets>/lib/poster-render.global.js, globalVariant of the file above),
// loaded with <script integrity> where a page's policy forbids importing a blob: of the verified text.
export const POSTER_RENDER_GLOBAL_SHA384 = 'sha384-idoDSzFXNuBK27dktAJffZD6QhubGkNOk+g9vwPobzs0HWHfnUq3cSyX7FyzwvtQ'
export const POSTER_RENDER_GLOBAL_SHA384 = 'sha384-N0M7S+HNC5jPovGXyhRpAVrGCgmjzt8tSxQ7GiZOnEeZ8FsBJZRVXeoOimLJhAea'
// The sha384 of library/scripts/posters/packet-strings.json, served at <assets>/lib/packet-strings.json. The
// packet, list and card text in every language, fetched at print time (packet/packetStrings.js) and used only
// when it matches; otherwise the print falls back to the bundled English block.
export const PACKET_STRINGS_SHA384 = 'sha384-YAzPNeMU+DF26OIEJrzo+Rr5f3lvoCjhae292a4tvGQZo5fggUQroiBVJCgBDtTF'
export const PACKET_STRINGS_SHA384 = 'sha384-NhjD/zyyBCJBF6Jj9IZ0UBtAbX5pw8hdKJTSXdVoahkzK82YpeyT2m733E0H0p4/'

View file

@ -287,14 +287,10 @@ const HEAD_COLUMNS_CSS = `
// at 22 / 21px with darker ink: about 8pt on the printed page. Only the full layout (HEAD_COLUMNS) gets this; the
// series-wide rules above stay as they were, so the series' SPARSE posters are byte-identical.
// Print size = sheet px x 0.92 (the packet's margins) x 0.408 (1500px = 8.5in): 22px ≈ 8.3pt, 19px ≈ 7.1pt.
// ★ THE NAMES COME FIRST (2026-10-03): the header columns step down to 18px (≈6.8pt; keys 15.5, headings 19) so
// the people-group names beside the map can be the larger type. The shorter band gives the map ~80px more height,
// and the names' cap on this layout rises to 19px (see MAXF). These sizes must equal HC's (fs, keyFs, headFs).
const HEAD_COLUMNS_FULL_CSS = `
.hcolk{font-size:15.5px;fill:#2f2a22;font-weight:800;letter-spacing:.1em}
.hcolt{font-size:18px;fill:#3a362c}
.hcolb{font-weight:700;fill:#2f2a22}
.hcolh{font-family:Georgia,'Times New Roman',serif;font-size:19px;font-weight:700;fill:#2f2a22;letter-spacing:.04em;font-variant:small-caps}
.hcolk{font-size:19px;fill:#2f2a22;font-weight:800;letter-spacing:.1em}
.hcolt{font-size:22px;fill:#3a362c}
.hcolh{font-family:Georgia,'Times New Roman',serif;font-size:23px;font-weight:700;fill:#2f2a22;letter-spacing:.04em;font-variant:small-caps}
.hvref{font-size:19px}
.hverse{font-size:19.5px}
.hsrc{font-size:22px;fill:#4a4636}
@ -694,9 +690,7 @@ function layoutLabels(pins, project) {
col.forEach((it) => { it.maxNum = MAXNUM_L; });
// A map-only sheet frees the header and footer bands the same way SPARSE frees the vision
// columns, so it gets SPARSE's ceiling; the solver still only uses what the column can hold.
// The three-column sheet (HEAD_COLUMNS) gets it too: there the names are the main text, and the cap of 15
// held 21 of the 28 zones' names at 15px (≈5.6pt printed) with room to spare in the band.
const MAXF = (SPARSE || MAP_ONLY || HEAD_COLUMNS) ? 19 : 15;
const MAXF = (SPARSE || MAP_ONLY) ? 19 : 15;
// AVAIL IS THE COLUMN, AND THE NAME DOES NOT GET ALL OF IT.
// The renderer draws the name starting one number-column IN from the column's
// map-facing edge (`nameEdge = tx ± numColW`) but does not shrink its budget by
@ -730,23 +724,18 @@ function layoutLabels(pins, project) {
if (line) out.push(line);
return out.length ? out : [it.name];
};
// The air between rows, as a share of the size. The three-column sheet sets it tighter (0.22): its densest
// zones (95-118 names) are held by the band's height, and the tighter rows turn that height into type
// (west-africa 12.5 -> 13.8px, laos 15.8 -> 17.3px). A row's underline sits 0.30 below its baseline and the
// next row's capitals start 0.56 below it, so they stay clear. Every other sheet keeps 0.34.
const GAPF = HEAD_COLUMNS ? 0.22 : 0.34;
let font = MAXF, wraps = null;
for (let f = MAXF; f >= 8; f -= 0.25) {
const ws = col.map((it) => wrapName(it, f));
const totalLines = ws.reduce((a, w) => a + w.length, 0);
// Each line needs f*1.06 of pitch, plus a little air between rows.
if (totalLines * (f * 1.06) + col.length * (f * GAPF) <= bandH) { font = f; wraps = ws; break; }
if (totalLines * (f * 1.06) + col.length * (f * 0.34) <= bandH) { font = f; wraps = ws; break; }
}
if (!wraps) wraps = col.map((it) => wrapName(it, font));
// Re-pitch rows by LINE COUNT so a two-line name gets the room it needs and
// single-line rows are not stretched to match it.
const lineH = font * 1.06;
const gap = font * GAPF;
const gap = font * 0.34;
const usedH = wraps.reduce((a, w) => a + w.length * lineH, 0) + col.length * gap;
const slack = Math.max(0, bandH - usedH) / Math.max(1, col.length);
let cursor = bandTop + slack / 2;
@ -841,8 +830,7 @@ function paraInto(push, text, { x, y, w, fs, lh, cls, lead }) {
// 1 Jesus' strategy to feed the 5000
// 2 the facts: a bold key above each value (countries, administrative areas, people groups, series)
// 3 what the series is
// The sizes equal HEAD_COLUMNS_FULL_CSS (.hcolt = fs, .hcolk = keyFs, .hcolh = headFs); leading and gaps scale with them.
const HC = { gut: 32, fs: 18, lh: 22, keyFs: 15.5, keyLh: 19.5, headLh: 24.5, gap: 7, headFs: 19 };
const HC = { gut: 32, fs: 22, lh: 27, keyFs: 19, keyLh: 24, headLh: 30, gap: 8, headFs: 23 };
// textW is calibrated for Lato. Without Lato the browser falls back to a system sans that sets
// these columns 2-6% wider (measured 1.05-1.06x in DejaVu Sans). Each block is centred on its
// width at 1.03x, halfway, so any overrun splits across both sides instead of all running right.
@ -863,50 +851,17 @@ const strategyParas = () => (POSTER_TEXT.strategyBody ? [].concat(POSTER_TEXT.st
// series.json (poster.sideBoxMore: one paragraph, or a list; [] prints none), as UPG100 does to keep its band even.
const whatIsMore = () => (POSTER_TEXT.sideBoxMore !== undefined ? [].concat(POSTER_TEXT.sideBoxMore) : [WHAT_IS_MORE]);
const WHAT_IS_MORE = 'Each map comes with a prayer list of its fifty to a hundred people groups — their names, a representative photo, and more detail — to mobilise informed intercession.';
// A paragraph's lines as SVG text content, its **...** runs set bold (.hcolb); the markers are not printed.
// The wrap measures the plain text: a short bold run is a few px wider, inside the measure's room.
function boldLines(text, width) {
let plain = '', bold = [], on = false;
for (const part of String(text).replace(/\s+/g, ' ').trim().split('**')) {
for (let i = 0; i < part.length; i++) { plain += part[i]; bold.push(on); } // per UTF-16 unit, as plain[p] reads
on = !on;
}
// A scripture reference never breaks between its book and its chapter:verse ("Mark 6:40"): that space becomes a
// placeholder for the wrap and prints as a space again. One character for one, so the bold flags stay aligned.
plain = plain.replace(/([A-Za-z]+) (\d+:\d+)/g, '$1\u0001$2');
let p = 0;
return wrapPx(plain, HC.fs, width).map((ln) => {
while (plain[p] === ' ') p++;
let out = '', run = '', runBold = null;
const flush = () => { if (run) out += runBold ? `<tspan class="hcolb">${esc(run)}</tspan>` : esc(run); run = ''; };
for (let k = 0; k < ln.length; k++, p++) {
if (bold[p] !== runBold) { flush(); runBold = bold[p]; }
run += plain[p] === '\u0001' ? ' ' : plain[p];
}
flush();
return out;
});
}
function headColumnsBand(top, facts, approxLists) {
const GRID_L = BAND_MARGIN_G, GRID_W = PW - 2 * BAND_MARGIN_G;
const AVAIL = GRID_W - 2 * HC.gut;
const CW = (GRID_W - 2 * HC.gut) / 3;
// The measure leaves room for a fallback font (see HC_FALLBACK): a block set to it stays inside its column.
const measure = (cw) => (cw - 10 - BAND_TEXT_PAD / 2) * 0.92;
// ★ EVEN COLUMNS (2026-10-03): when the facts column runs longer than the two text columns it is WIDENED (and
// shorter, it is narrowed) so the three end at about the same height. The two side columns stay equal, so they
// still mirror each other round the centre. The centre's share is searched from 26% to 46% of the three-column
// width; the split whose TALLEST column is shortest wins, and a tie goes to the split nearest equal widths, so a
// zone that is already balanced keeps equal columns. measureHeader calls this same function, so the map below
// gets exactly the height that is left.
const TW = (CW - 10 - BAND_TEXT_PAD / 2) * 0.92;
// Uppercase, letterspaced keys: measured as capitals plus the .14em tracking.
const keyW = (k) => textW(k.toUpperCase(), HC.keyFs) + k.length * HC.keyFs * 0.14;
// Each column's height at a given measure (the same arithmetic the drawing below advances y by).
const proseH = (col, tw) => HC.headLh + col[0].paras.reduce((h, t, k) => h + (k ? HC.gap : 0) + boldLines(t, tw).length * HC.lh, 0);
const factsH = (tw) => facts.reduce((h, [, value], k) => h + (k ? HC.gap : 0) + HC.keyLh + wrapPx(value, HC.fs, tw).length * HC.lh, 0);
// Column order (2026-10-02): Jesus' strategy | the facts | what the series is.
const cols = [
[{ head: 'Jesus’ Strategy to Feed the 5000', paras: strategyParas() }],
null, // the facts column, wrapped below at the centre's measure
facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, TW) })),
[{ head: POSTER_TEXT.sideBoxHeading || 'What is a UPG100 List?', paras: [
(POSTER_TEXT.sideBoxBody
|| 'This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.')
@ -914,22 +869,9 @@ function headColumnsBand(top, facts, approxLists) {
...whatIsMore(),
] }],
];
// The split: equal thirds first, then centre shares on a 1% step ordered by their distance from a third, so
// among splits whose tallest column is equally short, the one nearest equal widths is found first and kept.
const shares = Array.from({ length: 21 }, (_, k) => 0.26 + k * 0.01).sort((a, b) => Math.abs(a - 1 / 3) - Math.abs(b - 1 / 3));
let best = null;
for (const cwC of [1 / 3, ...shares].map((f) => AVAIL * f)) {
const cwS = (AVAIL - cwC) / 2;
const tall = Math.max(proseH(cols[0], measure(cwS)), factsH(measure(cwC)), proseH(cols[2], measure(cwS)));
if (!best || tall < best.tall - 0.5) best = { cwC, cwS, tall };
}
const widths = [best.cwS, best.cwC, best.cwS];
const lefts = [GRID_L, GRID_L + best.cwS + HC.gut, GRID_L + best.cwS + HC.gut + best.cwC + HC.gut];
cols[1] = facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, measure(best.cwC)) }));
let frags = '', bottom = top;
cols.forEach((col, i) => {
const CW = widths[i], TW = measure(CW);
const centre = lefts[i] + CW / 2;
const centre = GRID_L + i * (CW + HC.gut) + CW / 2;
if (col[0] && col[0].key !== undefined) { // the facts column: a bold key above each value
const blockW = Math.min(TW, Math.max(...col.map((r) => Math.max(keyW(r.key), ...r.lines.map((ln) => textW(ln, HC.fs))))));
const x = (centre - blockW * HC_FALLBACK / 2).toFixed(1);
@ -943,22 +885,20 @@ function headColumnsBand(top, facts, approxLists) {
bottom = Math.max(bottom, y);
} else {
const { head, paras } = col[0];
// ★ MIRRORED (2026-10-03): both prose columns take the full measure, so each sits the same distance from
// its rule and the two outer columns mirror each other round the centred facts column. (Each used to be
// centred on its own widest line, so the two could sit at different distances from their rules.)
const blockW = TW;
const wrapped = paras.map((t) => wrapPx(t, HC.fs, TW));
const blockW = Math.min(TW, Math.max(textW(head, HC.headFs), ...wrapped.flat().map((ln) => textW(ln, HC.fs))));
const x = (centre - blockW * HC_FALLBACK / 2).toFixed(1);
frags += `<text x="${x}" y="${top}" class="hcolh">${esc(head)}</text>`;
let y = top + HC.headLh;
paras.forEach((text, k) => {
wrapped.forEach((lines, k) => {
if (k) y += HC.gap;
for (const ln of boldLines(text, TW)) { frags += `<text x="${x}" y="${y}" class="hcolt">${ln}</text>`; y += HC.lh; }
for (const ln of lines) { frags += `<text x="${x}" y="${y}" class="hcolt">${esc(ln)}</text>`; y += HC.lh; }
});
bottom = Math.max(bottom, y);
}
});
for (let i = 1; i < 3; i++) {
const rx = (lefts[i] - HC.gut / 2).toFixed(1);
const rx = (GRID_L + i * (CW + HC.gut) - HC.gut / 2).toFixed(1);
frags += `<line x1="${rx}" y1="${top - 14}" x2="${rx}" y2="${bottom - 12}" class="hcolrule"/>`;
}
return { frags, bottom };
@ -2404,7 +2344,7 @@ async function renderPoster(zone, pins, zoneFC) {
let cy0 = bodyTop;
strategyParas().forEach((text, k) => {
if (k) cy0 += 5;
cy0 = paraInto(push, String(text).replace(/\*\*/g, ''), { x: b1x[0], y: cy0, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
cy0 = paraInto(push, text, { x: b1x[0], y: cy0, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
});
let cy1 = bodyTop;
@ -2415,11 +2355,9 @@ async function renderPoster(zone, pins, zoneFC) {
|| 'This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.')
.replace('{approxLists}', String(approxLists))
.replace('{seriesName}', SERIES_NAME);
// A **...** bold marker is the band's (boldLines); this classic header prints plain, so the markers are dropped.
const plain = (t) => String(t).replace(/\*\*/g, '');
[whatIs, ...whatIsMore()].forEach((text, k) => {
if (k) cy1 += 5;
cy1 = paraInto(push, plain(text), { x: b1x[1], y: cy1, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
cy1 = paraInto(push, text, { x: b1x[1], y: cy1, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
});
const b1Bot = Math.max(cy0, cy1);

View file

@ -1,14 +1,8 @@
// The Subresource-Integrity form of a text's SHA-384 ("sha384-<base64>"), via WebCrypto, so a file fetched at
// print time can be compared with the pin baked into the bundle (poster-render.integrity.js) before it is used.
// The same value pin-poster-render.mjs computes with node:crypto.
// ★ ONLY ON A SECURE PAGE. A browser gives crypto.subtle only to a secure page (https, or http://localhost). Opened any
// other way (a dev server reached over plain http by its LAN address, from a phone on the same Wi-Fi), every pinned
// file was refused with a bare "reading 'digest'": the packet printed in English and page 1 fell back to its stored
// sheet, with nothing saying why. Say it in words.
export async function sha384(text) {
const subtle = globalThis.crypto && globalThis.crypto.subtle
if (!subtle) throw new Error('pinned files can only be checked on a secure page (https, or http://localhost); this page is not one')
const d = new Uint8Array(await subtle.digest('SHA-384', new TextEncoder().encode(text)))
const d = new Uint8Array(await crypto.subtle.digest('SHA-384', new TextEncoder().encode(text)))
let bin = ''
for (const b of d) bin += String.fromCharCode(b)
return `sha384-${btoa(bin)}`

View file

@ -52,7 +52,7 @@
"n": 1,
"rhythm": "praise",
"sub": "Two-Cross Gospel",
"text": "Praise Jesus for taking up a cross for you and being humbled. Crown Him as your King, and pray for these nations (people groups) that they will do the same. Devote yourself to Jesus to take up your own cross and follow Him, even through suffering, and death. Praise Him for He is worthy of these nations (people groups) taking up their own cross, willing to follow Jesus no matter what the cost. May the Lamb receive the reward of His suffering, starting with me.",
"text": "That they accept Christ as King, forgiven by His cross — and take up their own cross, willing to suffer through persecution. May the Lamb receive the reward of His suffering, starting with me.",
"refs": [
"Matthew 27:27",
"Matthew 16:24–25"
@ -62,7 +62,7 @@
"n": 2,
"rhythm": "wait",
"sub": "Holy Spirit & deliverance",
"text": "Spend time waiting on the Holy Spirit to fill you and empower you to pray as a bold prayer warrior for them. Focus in on God's presence, and wait on the Lord!",
"text": "Spend time waiting on the Holy Spirit to tell you and empower you to pray as a bold prayer warrior for them: that they are filled with the Holy Spirit and made bold witnesses, not isolated; baptized in water and Spirit; every evil grip broken; and that believers in neighboring people groups are mobilized as laborers.",
"refs": [
"Acts 1:8",
"John 14:26"
@ -72,7 +72,7 @@
"n": 3,
"rhythm": "confess",
"sub": "",
"text": "Be open and honest with God about your fears, lies, and unbelief surrounding unreached people groups (Example lie: It is too big an ask for God to reach all of these unreached people groups in our lifetime). Receive His love, truth, and voice surrounding unreached people groups and your role in helping Him reach these people groups. Ask God to revive your love and faith for the people groups on your prayer list.",
"text": "We confess the unbelief that it is too big an ask for God to reach all of these unreached people groups in our lifetime. We receive the truth that He is reviving our love and faith for the global unreached.",
"refs": [
"John 3:16",
"1 John 1:9"
@ -81,8 +81,8 @@
{
"n": 4,
"rhythm": "read",
"sub": "the heavenly Father's love",
"text": "That they would be SAVED and KNOW their heavenly Father loves them — He wants to save them, and through them He wants to bring salvation to their family, friends, neighbors, coworkers, classmates, everyone they know personally.",
"sub": "the Father's love",
"text": "That they would be SAVED and KNOW their heavenly Father loves them — and that through them He would bring salvation to their family, friends, neighbours, coworkers, classmates, everyone they know personally.",
"refs": [
"Mark 5:18–20",
"Acts 16:31"
@ -93,7 +93,7 @@
"n": 5,
"rhythm": "ask",
"sub": "baptisms",
"text": "New baptisms among the indigenous people groups, and every baptized disciple is trained to baptize others — multiplying baptisms.",
"text": "New baptisms among the indigenous people groups, and every baptized disciple trained to baptize others — multiplying baptisms.",
"refs": [
"Acts 16",
"Acts 10"
@ -103,7 +103,7 @@
"n": 6,
"rhythm": "intercede",
"sub": "laborers & shepherds",
"text": "For laborers sent to deeply love a people they have never met; and indigenous good shepherds raised up from within the people groups.",
"text": "Laborers sent to deeply love a people they have never met; good shepherds raised up from within.",
"refs": [
"Luke 10:2",
"John 10:11–15"
@ -113,7 +113,7 @@
"n": 7,
"rhythm": "pray_word",
"sub": "Persons & Houses of Peace",
"text": "That laborers and indigenous disciples find the receptive person and the house of peace, deeply love them, disciple them well, baptize them, and train them to reach their friends and family with their newfound love for Jesus.",
"text": "That laborers and indigenous leaders find the receptive person and the house of peace, deeply love them, disciple them well, baptize them, and train them to reach their friends and family with their newfound love for Jesus.",
"refs": [
"Luke 10:5–7",
"Matthew 10:5–14"
@ -133,17 +133,17 @@
"n": 9,
"rhythm": "sing",
"sub": "",
"text": "Imagine you are with Jesus worshipping Him, and you are surrounded by all the people groups on your prayer list worshipping Him too. Spend some time to praise Jesus.",
"text": "That the people group is flooded with worshippers — worship everywhere, in freedom and in chains.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
"n": 10,
"rhythm": "meditate",
"sub": "communion",
"text": "Focus on Jesus. Take communion if possible. Imagine being together with Jesus at a table with one person from each people group on your list. What is it going to take for this to become a reality?",
"text": "That they take communion together and grasp the freedom they have in Christ through His death, burial and resurrection.",
"refs": [
"Matthew 26:26–30",
"1 Corinthians 11:23–26"
@ -163,10 +163,11 @@
"n": 12,
"rhythm": "praise_end",
"sub": "blessing · great · greater · greatest",
"text": "Short reading: It is a blessing from God for people in these people groups to be saved through Jesus and have eternal life (John 17:3). It is a great blessing from God for indigenous disciples to lead their friends, family, coworkers, classmates, neighbors, and people they don't know, to Jesus through their witness (John 17:20). It is a greater blessing from God when indigenous leaders in these people groups start new churches that are unified in Jesus (John 17:21). It is one of the greatest blessings from God, when indigenous leaders train other indigenous leaders to start more churches unified in Christ. They begin to worship Jesus and spread throughout neighboring unreached people groups bringing the life of Jesus to the ends of the earth. Spend time singing praise to God. He is worthy of worshippers in all these people groups!",
"text": "Pray Jesus’ prayer in John 17 over them. The blessing: eternal life, knowing God and Jesus, whom He sent. The great blessing: their own believers leading others to faith. The greater blessing: new spiritual families of their own, one in Christ. One of the greatest: leaders from among them who train others to start new spiritual families, again and again. Ask all this and more, beyond all we ask or think. Ask, as Jesus did, that they be one and kept from the evil one. None of it is possible unless Jesus is who He says He is.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -174,8 +175,8 @@
"point4_passage_credit": "Mark 5:18–20 · World English Bible (public domain)",
"point4_passage_bold": "He didn’t allow him, but said to him, “Go to your house, to your friends, and tell them what great things the Lord has done for you and how he had mercy on you.”",
"point4_heart_title": "THE HEAVENLY FATHER'S HEART",
"point4_heart_lede": "They need to know that their heavenly Father loves them, He wants to save them, and through them He wants their unreached people group neighbors to be saved.",
"point4_heart_leadin": "More scriptures about the heavenly Father's heart (pick one scripture to read)",
"point4_heart_lede": "They need to know that their Heavenly Father loves them, He wants to save them, and through them He wants their unreached people group neighbors to be saved.",
"point4_heart_leadin": "More scriptures about the heavenly Father's heart",
"point4_heart": [
{
"who": "Noah",
@ -262,7 +263,6 @@
"sets_bounded_title": "Reached, and not",
"sets_centered_title": "Distance from a neighbour",
"sets_bounded_tally": "{inside} inside · {outside} outside · {unknown} unknown",
"sets_no_data": "{n} without data",
"sets_centered_tally": "{placed} placed · {centre} at the centre · {unplaced} unplaced",
"ring_band_dialect": "Same dialect",
"ring_band_language": "Same language",
@ -297,8 +297,7 @@
"Haggai": "Haggai",
"Isaiah": "Isaiah",
"Habakkuk": "Habakkuk",
"Ephesians": "Ephesians",
"Revelation": "Revelation"
"Ephesians": "Ephesians"
},
"christian_per_cell": "Each box ≈ {n} people",
"howto_title": "How to pray with this list",
@ -489,7 +488,7 @@
"text": "Que el grupo étnico sea inundado de adoradores — adoración en todas partes, en libertad y en cadenas.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -518,8 +517,9 @@
"sub": "bendición · grande · mayor · de las mayores",
"text": "Ora sobre ellos la oración de Jesús en Juan 17. La bendición: la vida eterna, conocer a Dios y a Jesús, a quien él envió. La gran bendición: que sus propios creyentes lleven a otros a la fe. La bendición mayor: nuevas familias espirituales propias, unidas en Cristo. Una de las mayores: líderes de entre ellos que formen a otros para iniciar nuevas familias espirituales, una y otra vez. Pide todo esto y más, mucho más de lo que pedimos o pensamos. Pide, como Jesús, que sean uno y que sean guardados del maligno. Nada de esto es posible si Jesús no es quien dice ser.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -608,7 +608,6 @@
"sets_bounded_title": "Alcanzados y no alcanzados",
"sets_centered_title": "Distancia a un vecino",
"sets_bounded_tally": "{inside} dentro · {outside} fuera · {unknown} desconocido",
"sets_no_data": "{n} sin datos",
"sets_centered_tally": "{placed} ubicados · {centre} en el centro · {unplaced} sin ubicar",
"ring_band_dialect": "Mismo dialecto",
"ring_band_language": "Misma lengua",
@ -643,8 +642,7 @@
"Haggai": "Hageo",
"Isaiah": "Isaías",
"Habakkuk": "Habacuc",
"Ephesians": "Efesios",
"Revelation": "Apocalipsis"
"Ephesians": "Efesios"
},
"christian_per_cell": "Cada casilla ≈ {n} personas",
"howto_title": "Cómo orar con esta lista",
@ -821,7 +819,7 @@
"text": "Que le groupe ethnique soit inondé d'adorateurs — l'adoration partout, dans la liberté comme dans les chaînes.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -850,8 +848,9 @@
"sub": "bénédiction · grande · plus grande · parmi les plus grandes",
"text": "Priez sur eux la prière de Jésus en Jean 17. La bénédiction : la vie éternelle, connaître Dieu et Jésus, celui qu’il a envoyé. La grande bénédiction : que leurs propres croyants amènent d’autres à la foi. Une bénédiction plus grande : de nouvelles familles spirituelles bien à eux, unies en Christ. L’une des plus grandes : des responsables issus de leur peuple qui forment d’autres personnes à fonder de nouvelles familles spirituelles, encore et encore. Demandez tout cela et plus encore, au-delà de tout ce que nous demandons ou pensons. Demandez, comme Jésus, qu’ils soient un et gardés du malin. Rien de cela n’est possible si Jésus n’est pas celui qu’il dit être.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -940,7 +939,6 @@
"sets_bounded_title": "Atteints, ou non",
"sets_centered_title": "Distance d’un voisin",
"sets_bounded_tally": "{inside} à l’intérieur · {outside} à l’extérieur · {unknown} inconnu",
"sets_no_data": "{n} sans données",
"sets_centered_tally": "{placed} placés · {centre} au centre · {unplaced} non placés",
"ring_band_dialect": "Même dialecte",
"ring_band_language": "Même langue",
@ -975,8 +973,7 @@
"Haggai": "Aggée",
"Isaiah": "Ésaïe",
"Habakkuk": "Habacuc",
"Ephesians": "Éphésiens",
"Revelation": "Apocalypse"
"Ephesians": "Éphésiens"
},
"christian_per_cell": "Chaque case ≈ {n} personnes",
"howto_title": "Comment prier avec cette liste",
@ -1153,7 +1150,7 @@
"text": "Que o grupo étnico seja inundado de adoradores — adoração em toda parte, em liberdade e em correntes.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -1182,8 +1179,9 @@
"sub": "bênção · grande · maior · das maiores",
"text": "Ore sobre eles a oração de Jesus em João 17. A bênção: a vida eterna, conhecer a Deus e a Jesus, a quem ele enviou. A grande bênção: que os seus próprios crentes levem outros à fé. A bênção maior: novas famílias espirituais deles mesmos, unidas em Cristo. Uma das maiores: líderes do meio deles que preparem outros para começar novas famílias espirituais, vez após vez. Peça tudo isso e mais, muito além do que pedimos ou pensamos. Peça, como Jesus pediu, que sejam um e guardados do maligno. Nada disso é possível se Jesus não for quem ele diz ser.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -1269,7 +1267,6 @@
"sets_bounded_title": "Alcançados e não alcançados",
"sets_centered_title": "Distância de um vizinho",
"sets_bounded_tally": "{inside} dentro · {outside} fora · {unknown} desconhecido",
"sets_no_data": "{n} sem dados",
"sets_centered_tally": "{placed} posicionados · {centre} no centro · {unplaced} sem posição",
"ring_band_dialect": "Mesmo dialeto",
"ring_band_language": "Mesma língua",
@ -1304,8 +1301,7 @@
"Haggai": "Ageu",
"Isaiah": "Isaías",
"Habakkuk": "Habacuque",
"Ephesians": "Efésios",
"Revelation": "Apocalipse"
"Ephesians": "Efésios"
},
"christian_per_cell": "Cada caixa ≈ {n} pessoas",
"howto_title": "Como orar com esta lista",
@ -1482,7 +1478,7 @@
"text": "Dass die Volksgruppe von Anbetern überflutet wird — Anbetung überall, in Freiheit und in Ketten.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -1511,8 +1507,9 @@
"sub": "Segen · groß · größer · einer der größten",
"text": "Beten Sie Jesu Gebet aus Johannes 17 über ihnen. Der Segen: ewiges Leben, Gott zu kennen und Jesus, den er gesandt hat. Der große Segen: dass ihre eigenen Gläubigen andere zum Glauben führen. Der größere Segen: neue geistliche Familien, ihre eigenen, eins in Christus. Einer der größten: Leiter aus ihrer Mitte, die andere anleiten, neue geistliche Familien zu gründen, immer wieder. Bitten Sie um all das und mehr, über alles hinaus, was wir bitten oder verstehen. Bitten Sie wie Jesus, dass sie eins seien und vor dem Bösen bewahrt werden. Nichts davon ist möglich, wenn Jesus nicht der ist, der er zu sein sagt.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -1601,7 +1598,6 @@
"sets_bounded_title": "Erreicht und nicht erreicht",
"sets_centered_title": "Abstand zu einem Nachbarn",
"sets_bounded_tally": "{inside} innen · {outside} außen · {unknown} unbekannt",
"sets_no_data": "{n} ohne Daten",
"sets_centered_tally": "{placed} platziert · {centre} in der Mitte · {unplaced} ohne Platz",
"ring_band_dialect": "Gleicher Dialekt",
"ring_band_language": "Gleiche Sprache",
@ -1636,8 +1632,7 @@
"Haggai": "Haggai",
"Isaiah": "Jesaja",
"Habakkuk": "Habakuk",
"Ephesians": "Epheser",
"Revelation": "Offenbarung"
"Ephesians": "Epheser"
},
"christian_per_cell": "Jedes Kästchen ≈ {n} Menschen",
"howto_title": "So beten Sie mit dieser Liste",
@ -1814,7 +1809,7 @@
"text": "Che il gruppo etnico sia inondato di adoratori — adorazione ovunque, nella libertà e nelle catene.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -1843,8 +1838,9 @@
"sub": "benedizione · grande · più grande · tra le più grandi",
"text": "Prega su di loro la preghiera di Gesù in Giovanni 17. La benedizione: la vita eterna, conoscere Dio e Gesù, colui che egli ha mandato. La grande benedizione: che i loro stessi credenti portino altri alla fede. La benedizione più grande: nuove famiglie spirituali tutte loro, unite in Cristo. Una delle più grandi: responsabili del loro popolo che formano altri ad avviare nuove famiglie spirituali, ancora e ancora. Chiedi tutto questo e di più, oltre tutto ciò che chiediamo o pensiamo. Chiedi, come Gesù, che siano uno e custoditi dal maligno. Niente di tutto questo è possibile se Gesù non è chi dice di essere.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -1930,7 +1926,6 @@
"sets_bounded_title": "Raggiunti e non raggiunti",
"sets_centered_title": "Distanza da un vicino",
"sets_bounded_tally": "{inside} dentro · {outside} fuori · {unknown} sconosciuto",
"sets_no_data": "{n} senza dati",
"sets_centered_tally": "{placed} collocati · {centre} al centro · {unplaced} non collocati",
"ring_band_dialect": "Stesso dialetto",
"ring_band_language": "Stessa lingua",
@ -1965,8 +1960,7 @@
"Haggai": "Aggeo",
"Isaiah": "Isaia",
"Habakkuk": "Abacuc",
"Ephesians": "Efesini",
"Revelation": "Apocalisse"
"Ephesians": "Efesini"
},
"christian_per_cell": "Ogni casella ≈ {n} persone",
"howto_title": "Come pregare con questa lista",
@ -2143,7 +2137,7 @@
"text": "Grupul etnic să fie inundat de închinători — închinare pretutindeni, în libertate și în lanțuri.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -2172,8 +2166,9 @@
"sub": "binecuvântare · mare · mai mare · printre cele mai mari",
"text": "Roagă-te peste ei cu rugăciunea lui Isus din Ioan 17. Binecuvântarea: viața veșnică, să-L cunoască pe Dumnezeu și pe Isus, pe care L-a trimis. Marea binecuvântare: ca propriii lor credincioși să-i aducă pe alții la credință. O binecuvântare și mai mare: familii spirituale noi, ale lor, una în Hristos. Una dintre cele mai mari: lideri dintre ei care îi pregătesc pe alții să înceapă noi familii spirituale, iar și iar. Cere toate acestea și mai mult, dincolo de tot ce cerem sau gândim. Cere, ca Isus, să fie una și păziți de cel rău. Nimic din toate acestea nu este posibil dacă Isus nu este Cel care spune că este.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -2259,7 +2254,6 @@
"sets_bounded_title": "Atinse și neatinse",
"sets_centered_title": "Distanța față de un vecin",
"sets_bounded_tally": "{inside} înăuntru · {outside} în afară · {unknown} necunoscut",
"sets_no_data": "{n} fără date",
"sets_centered_tally": "{placed} plasate · {centre} în centru · {unplaced} neplasate",
"ring_band_dialect": "Același dialect",
"ring_band_language": "Aceeași limbă",
@ -2294,8 +2288,7 @@
"Haggai": "Hagai",
"Isaiah": "Isaia",
"Habakkuk": "Habacuc",
"Ephesians": "Efeseni",
"Revelation": "Apocalipsa"
"Ephesians": "Efeseni"
},
"christian_per_cell": "Fiecare căsuță ≈ {n} de oameni",
"howto_title": "Cum să te rogi cu această listă",
@ -2470,7 +2463,7 @@
"text": "Чтобы народ наполнился поклонниками — поклонение повсюду, в свободе и в цепях.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -2499,8 +2492,9 @@
"sub": "благословение · великое · большее · одно из величайших",
"text": "Молитесь над ними молитвой Иисуса из Иоанна 17. Благословение: вечная жизнь — знать Бога и Иисуса, Которого Он послал. Великое благословение: чтобы их собственные верующие приводили других к вере. Ещё большее благословение: новые духовные семьи, свои собственные, единые во Христе. Одно из величайших: лидеры из их среды, которые учат других начинать новые духовные семьи — снова и снова. Просите обо всём этом и о большем — превыше всего, чего мы просим или о чём помышляем. Просите, как Иисус, чтобы они были едины и сохранены от лукавого. Ничто из этого невозможно, если Иисус не Тот, Кем Он Себя называет.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -2589,7 +2583,6 @@
"sets_bounded_title": "Достигнутые и нет",
"sets_centered_title": "Расстояние до соседа",
"sets_bounded_tally": "{inside} внутри · {outside} снаружи · {unknown} неизвестно",
"sets_no_data": "{n} без данных",
"sets_centered_tally": "{placed} размещено · {centre} в центре · {unplaced} без места",
"ring_band_dialect": "Тот же диалект",
"ring_band_language": "Тот же язык",
@ -2624,8 +2617,7 @@
"Haggai": "Агг.",
"Isaiah": "Ис.",
"Habakkuk": "Авв.",
"Ephesians": "Еф.",
"Revelation": "Откр."
"Ephesians": "Еф."
},
"christian_per_cell": "Каждая клетка ≈ {n} чел.",
"howto_title": "Как молиться по этому списку",
@ -2809,7 +2801,7 @@
"text": "أن يفيض الشعب بالساجدين — عبادة في كل مكان، في الحرية وفي القيود.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -2838,8 +2830,9 @@
"sub": "البركة · العظيمة · الأعظم · من أعظمها",
"text": "صلِّ لأجلهم بصلاة يسوع في يوحنا ١٧. البركة: الحياة الأبدية، أن يعرفوا الله ويسوع الذي أرسله. البركة العظيمة: أن يقود المؤمنون منهم غيرهم إلى الإيمان. البركة الأعظم: عائلات روحية جديدة خاصة بهم، واحدة في المسيح. ومن أعظم البركات: قادة من بينهم يدرّبون آخرين ليبدؤوا عائلات روحية جديدة، مرة بعد مرة. اطلب كل هذا وأكثر، أكثر جدًا مما نطلب أو نفتكر. واطلب، كما طلب يسوع، أن يكونوا واحدًا وأن يُحفظوا من الشرير. لا شيء من هذا ممكن إن لم يكن يسوع هو مَن يقول إنه هو.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -2928,7 +2921,6 @@
"sets_bounded_title": "وصلتهم البشارة ومن لم تصلهم",
"sets_centered_title": "المسافة إلى جار",
"sets_bounded_tally": "{inside} في الداخل · {outside} في الخارج · {unknown} غير معروف",
"sets_no_data": "{n} بلا بيانات",
"sets_centered_tally": "{placed} موضوعون · {centre} في المركز · {unplaced} بلا موضع",
"ring_band_dialect": "اللهجة نفسها",
"ring_band_language": "اللغة نفسها",
@ -2963,8 +2955,7 @@
"Haggai": "حجي",
"Isaiah": "إشعياء",
"Habakkuk": "حبقوق",
"Ephesians": "أفسس",
"Revelation": "رؤيا"
"Ephesians": "أفسس"
},
"christian_per_cell": "كل مربّع ≈ {n} شخص",
"howto_title": "كيف تصلّي بهذه القائمة",
@ -3141,7 +3132,7 @@
"text": "कि वह जनसमूह आराधकों से भर जाए — हर जगह आराधना, स्वतंत्रता में और बेड़ियों में भी।",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -3170,8 +3161,9 @@
"sub": "आशीष · बड़ी · और बड़ी · सबसे बड़ी में से एक",
"text": "यूहन्ना 17 में यीशु ने जो प्रार्थना की, वही उनके लिए करें। आशीष: अनन्त जीवन — परमेश्वर को और यीशु को जानना, जिन्हें उन्होंने भेजा। बड़ी आशीष: उन्हीं के विश्वासी दूसरों को विश्वास में लाएँ। उससे बड़ी आशीष: उनके अपने नए आत्मिक परिवार, मसीह में एक। सबसे बड़ी आशीषों में से एक: उनके बीच से अगुवे, जो दूसरों को नए आत्मिक परिवार शुरू करने के लिए तैयार करें — बार-बार। यह सब और इससे भी अधिक माँगें, जितना हम माँगते या सोचते हैं उससे कहीं बढ़कर। यीशु की तरह माँगें कि वे एक हों और उस दुष्ट से बचे रहें। इनमें से कुछ भी संभव नहीं, यदि यीशु वह नहीं हैं जो वह कहते हैं कि वह हैं।",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -3257,7 +3249,6 @@
"sets_bounded_title": "पहुँचे और नहीं पहुँचे",
"sets_centered_title": "पड़ोसी से दूरी",
"sets_bounded_tally": "{inside} भीतर · {outside} बाहर · {unknown} अज्ञात",
"sets_no_data": "{n} बिना आँकड़ों के",
"sets_centered_tally": "{placed} स्थित · {centre} केंद्र में · {unplaced} अस्थित",
"ring_band_dialect": "वही बोली",
"ring_band_language": "वही भाषा",
@ -3292,8 +3283,7 @@
"Haggai": "हाग्गै",
"Isaiah": "यशायाह",
"Habakkuk": "हबक्कूक",
"Ephesians": "इफिसियों",
"Revelation": "प्रकाशितवाक्य"
"Ephesians": "इफिसियों"
},
"christian_per_cell": "हर खाना ≈ {n} लोग",
"howto_title": "इस सूची के साथ प्रार्थना कैसे करें",
@ -3468,7 +3458,7 @@
"text": "愿这族群充满敬拜者——处处敬拜,在自由中,也在捆锁中。",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -3497,8 +3487,9 @@
"sub": "祝福 · 大 · 更大 · 最大之一",
"text": "用耶稣在约翰福音17章的祷告为他们祷告。祝福:永生,认识神和他所差来的耶稣。大祝福:他们自己的信徒带领别人信主。更大的祝福:属于他们自己的新属灵家庭,在基督里合而为一。最大的祝福之一:从他们中间兴起领袖,装备别人建立新的属灵家庭,一代又一代。求这一切,还有更多,超过我们所求所想的。也像耶稣一样,求神使他们合而为一,保守他们脱离那恶者。若耶稣不是他自己所说的那一位,这一切都不可能。",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -3584,7 +3575,6 @@
"sets_bounded_title": "已触及与未触及",
"sets_centered_title": "与邻近族群的距离",
"sets_bounded_tally": "{inside} 圈内 · {outside} 圈外 · {unknown} 未知",
"sets_no_data": "{n} 无数据",
"sets_centered_tally": "{placed} 已定位 · {centre} 在中心 · {unplaced} 未定位",
"ring_band_dialect": "同一方言",
"ring_band_language": "同一语言",
@ -3619,8 +3609,7 @@
"Haggai": "哈该书",
"Isaiah": "以赛亚书",
"Habakkuk": "哈巴谷书",
"Ephesians": "以弗所书",
"Revelation": "启示录"
"Ephesians": "以弗所书"
},
"christian_per_cell": "每格约 {n} 人",
"howto_title": "如何使用这份祷告名单",

View file

@ -29,7 +29,7 @@
"titleQualifier": "the coloured words before the region name, e.g. 'Unengaged of' (:2427)",
"seriesName": "the name in quotes on the Series line and in the side box (:1877, :1907, :2115)",
"sideBoxHeading": "the right-hand explainer's heading (:2097)",
"sideBoxBody": "that explainer's first paragraph (:2115); in the head-columns band **...** prints a bold run",
"sideBoxBody": "that explainer's first paragraph (:2115)",
"strategyBody": "the Jesus column's text: one paragraph, or a list of them; absent = poster-render's STRATEGY_DEFAULT",
"sideBoxMore": "the explainer's further paragraphs: one, or a list; [] prints none; absent = poster-render's WHAT_IS_MORE"
},
@ -68,8 +68,8 @@
"titleQualifier": "Unengaged of",
"seriesName": "UPG100 List",
"sideBoxHeading": "What is a UPG100 List?",
"sideBoxBody": "This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists. Each map comes with a prayer list of its fifty to a hundred people groups — their names, a representative photo, and more detail — to mobilise **informed intercession.**",
"strategyBody": "2000 People Groups? Jesus’ disciples were faced with a similar problem, “How can we feed so many with only five loaves and two fish?” Through radical faith and miracle-expectant prayer, He focused on the few in front of Him — dividing the crowd and entrusting faithful men and women to serve the most strategic groups of fifties and hundreds (Mark 6:40).",
"sideBoxBody": "This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.",
"strategyBody": "Jesus fed the 5,000 by multiplying a small offering. He had the crowd sit in groups of hundreds and fifties (Mark 6:40). So we pray by name, a few groups at a time.",
"sideBoxMore": [],
"headColumns": true
},

View file

@ -212,24 +212,6 @@ export function absoluteEmbedUrl(leaf, profileConfig = null) {
return rel
}
/**
* The tree's PUBLIC address, for the Share menu's Embed tab (no trailing slash). Its URL, Iframe and JavaScript codes
* point here, so an embed follows the stable releases served there, never a dev server or the page's own host.
* Ladder: profileConfig.shareBaseUrl → window.MAP_SHARE_BASE_URL → the build's shareBaseUrl (vite.doxa-maps-build-
* config.json, baked as __DOXA_SHARE_BASE__) → '' (unset: the Embed tab keeps the tree the bundle was loaded from).
* @param {object|null} [profileConfig] the parsed profile-config (already unref'd)
* @returns {string}
*/
export function resolvePublicBase(profileConfig) {
// https only (an http base would give mixed-content codes on an https partner page), plus a local test server
const ok = (v) => typeof v === 'string' && /^(https:\/\/|http:\/\/(127\.0\.0\.1|localhost)(:\d+)?\/)/i.test(v.trim())
const fromConfig = profileConfig?.shareBaseUrl
if (ok(fromConfig)) return stripSlashes(fromConfig)
if (typeof window !== 'undefined' && ok(window.MAP_SHARE_BASE_URL)) return stripSlashes(window.MAP_SHARE_BASE_URL)
// typeof-guarded: a build or a test that defines no __DOXA_SHARE_BASE__ reads '' instead of throwing
return stripSlashes(typeof __DOXA_SHARE_BASE__ === 'string' ? __DOXA_SHARE_BASE__ : '')
}
export function resolveShareUrl({ shareUrl, embedPath, profileConfig } = {}) {
const cfg = profileConfig || null
const hasWindow = typeof window !== 'undefined'

View file

@ -1,73 +0,0 @@
/**
* publicEmbed.js: the Share menu's Embed codes for THIS plugin, from the tree's PUBLIC address.
*
* Three codes, all for the plugin itself (its own leaf page, its own bundle + element), never for the app it is
* nested in (Plugin Studio, Deep Research, Posterverse):
* URL <base>/<bundle>/<profile>/index.html, the plugin's generated page;
* Iframe <iframe src="that URL">;
* JavaScript the plugin's own embed.script from the PUBLIC manifest (<base>/manifest.json), made absolute.
*
* ★ THE SCRIPT IS READ FROM THE PUBLIC MANIFEST, NOT BUILT HERE. The manifest's per-profile embed.script is the one
* source of truth for the element tag, its profile-config and the page globals a plugin needs; the PUBLIC copy is the
* release partners will load, so its tag and config match what they get. It is fetched once per base and cached
* (the public host serves it with Access-Control-Allow-Origin: *).
*
* ★ NO INTEGRITY ON OUR BUNDLE. The public bundle URL is not versioned and is rebuilt in place at each release, so a
* pinned integrity="sha384-…" would break every partner's embed at the next release. It is dropped from the copied
* code for scripts served from the base; a versioned third-party URL (the Mapbox preload) keeps its own. Versioned
* release URLs + SRI are a later improvement.
*/
const cache = new Map() // base → Promise<manifest|null>
/** The tree-relative leaf path of an embedPath ('./x/y/index.html' → 'x/y/index.html'); '' for absolute or root paths. */
export function treeRelative(embedPath) {
const p = String(embedPath || '').trim()
if (!p || /^https?:\/\//i.test(p) || p.startsWith('/')) return ''
return p.replace(/^\.\//, '')
}
/** The plugin's public page: <base>/<leaf>. '' when either is unknown. */
export const publicLeafUrl = (base, embedPath) => {
const rel = treeRelative(embedPath)
return base && rel ? base + '/' + rel : ''
}
function loadManifest(base) {
if (!cache.has(base)) {
// the default credentials ('same-origin'): nothing is sent to a cross-origin public host, and behind a login gate
// a same-origin base keeps its session
const p = fetch(base + '/manifest.json')
.then((r) => (r.ok ? r.json() : null))
.catch(() => { cache.delete(base); return undefined }) // a network failure may be retried later
cache.set(base, p)
}
return cache.get(base)
}
/**
* The plugin's JavaScript embed code from the PUBLIC manifest.
* Resolves { code } | { missing: true } (the public site does not carry this plugin yet) | { failed: true }.
*/
export async function publicScriptCode(base, embedPath) {
const rel = treeRelative(embedPath)
if (!base || !rel) return { missing: true }
const m = await loadManifest(base)
if (m === undefined) return { failed: true }
if (!m || !Array.isArray(m.bundles)) return { missing: true }
const norm = (u) => String(u || '').replace(/^\.\//, '')
const [bundle, profile] = rel.split('/')
let hit = null
for (const b of m.bundles) for (const pr of (b.profiles || [])) {
if (norm(pr.iframeUrl) === rel) hit = pr
else if (!hit && b.bundle === bundle && pr.profile === profile) hit = pr // an instance path: its profile's code
}
const raw = hit && hit.embed && hit.embed.script
if (!raw) return { missing: true }
const esc = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const code = raw
.replace(/(\s(?:src|href)=")\.\//g, '$1' + base + '/')
.replace(/YOUR_TREE_URL/g, base)
.replace(new RegExp('(<script\\b[^>]*\\ssrc="' + esc + '/[^"]*")\\s+integrity="[^"]*"', 'g'), '$1')
return { code }
}

View file

@ -69,8 +69,6 @@
"check:describe-group": "node internal/check-describe-group.mjs",
"check:descriptions": "node internal/check-profile-descriptions.mjs",
"check:dev-assets": "node internal/check-dev-assets.mjs",
"check:dev-leaf-escape": "node internal/check-dev-leaf-escape.mjs",
"check:dev-leaf-escape:self-test": "node internal/check-dev-leaf-escape.mjs --self-test",
"check:doc-scripts": "node internal/check-doc-quotes-scripts.mjs",
"check:readme-size": "node internal/check-readme-web-folder-size.mjs",
"check:doc-scripts:self-test": "node internal/check-doc-quotes-scripts.mjs --self-test",
@ -118,8 +116,6 @@
"check:gospel-all": "node internal/check-gospel-all.mjs",
"check:gospel-internet": "node plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs && node plugin-designs/gospel-internet/scripts/check-gospel-internet-i18n.mjs && node plugin-designs/gospel-internet/scripts/check-language-tree.mjs && node plugin-designs/gospel-internet/scripts/check-gospel-internet-render.mjs",
"check:gospel-tables-staged": "node internal/check-gospel-tables-staged.mjs",
"check:gospel-clear-filter-url": "node plugin-designs/gospel-resources/scripts/check-clear-filter-url.mjs && node plugin-designs/gospel-resources/scripts/check-clear-filter-url.mjs --poison",
"check:gospel-template-translators": "node plugin-designs/gospel-resources/scripts/check-template-translators.mjs && node plugin-designs/gospel-resources/scripts/check-template-translators.mjs --poison",
"check:gospel-whiteboard": "node internal/check-gospel-whiteboard.mjs",
"check:hardcoded-english": "node internal/check-hardcoded-english.mjs",
"check:hardcoded-english:self-test": "node internal/check-hardcoded-english.mjs --self-test",
@ -363,7 +359,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:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test check:preview-flag-dev-only:self-test check:poster-type-registry:self-test check:host-floor:self-test check:phone-print-keep:self-test check:pinned-fetch-fresh:self-test",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:fork-leaders check:circular-layout check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:upg100-zone-copy check:upg100-zone-copy:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor check:phone-print-keep check:pinned-fetch-fresh check:view-address check:view-address:self-test check:dev-leaf-escape check:dev-leaf-escape:self-test check:gospel-clear-filter-url check:gospel-template-translators",
"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:pins-in-state check:fork-leaders check:circular-layout check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:upg100-zone-copy check:upg100-zone-copy:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor check:phone-print-keep check:pinned-fetch-fresh check:view-address check:view-address:self-test",
"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",
@ -449,7 +445,7 @@
"check:pinned-fetch-fresh:self-test": "bun internal/check-pinned-fetch-fresh.mjs --self-test"
},
"//scripts": {
"check:pinned-fetch-fresh": "a STALE cached copy of a pinned print-time file never decides a print: library/packet/sha384.js fetchPinned fetches force-cache, and on a copy that is not served or fails its pin, ONCE more with cache:'reload'; every pinned loader (posterSheets poster-render.js + ne-places.json, packetStrings, indiaStates) calls it, and the classic-script path refreshes the cache and retries a refused tag once. With a modelled HTTP cache holding stale copies: the current file after one fresh refetch; a matching cached copy used with one force-cache request; wrong bytes refused; page 1 still draws (generateSheet), the packet keeps every language, India keeps its 36 states. Self-test poisons: no fresh refetch, never the cache, a mismatch accepted, an unguarded crypto.subtle, a loader fetching its pinned file directly. On a page that is not secure (crypto.subtle missing: plain http by LAN address) the refusal says so in words (arm D2).",
"check:pinned-fetch-fresh": "a STALE cached copy of a pinned print-time file never decides a print: library/packet/sha384.js fetchPinned fetches force-cache, and on a copy that is not served or fails its pin, ONCE more with cache:'reload'; every pinned loader (posterSheets poster-render.js + ne-places.json, packetStrings, indiaStates) calls it, and the classic-script path refreshes the cache and retries a refused tag once. With a modelled HTTP cache holding stale copies: the current file after one fresh refetch; a matching cached copy used with one force-cache request; wrong bytes refused; page 1 still draws (generateSheet), the packet keeps every language, India keeps its 36 states. Self-test poisons: no fresh refetch, never the cache, a mismatch accepted, a loader fetching its pinned file directly.",
"check:phone-print-keep": "on a PHONE the in-page print document (#dxm-print-root and its print style, library/composables/printInPlace.js) and printImageBlob's object URL outlive afterprint, because Android Chrome fires afterprint once the PREVIEW is built and renders a saved PDF later, from the page as it is then (a phone's saved packet was the bare host page). A timeline on fake timers and a stub DOM with an Android user agent: still mounted 30 s after afterprint; released once the reader is back (shown again after hidden, focused again after blur, or touched after print() but not in its first TOUCH_SETTLE_MS) plus BACK_GRACE_MS (60 s), or at the ceiling; going away again during the grace cancels the pending drop; the Print tap of a late print does not count; a new print replaces it; desktop is the frame path, verbatim. --impl=<path> runs it against another copy (the pre-fix file fails). Self-test poisons: the old drop at afterprint, no release when back, listeners armed at mount, no grace, printImageBlob revoking at afterprint, no ceiling, no cancel when away again, no touch settle window.",
"_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.",
"browser-gates": "lists every package.json script that launches a browser (the same detection run-parallel uses to route them through internal/browser-slot.sh). Run each as internal/browser-slot.sh bun run <name>.",
@ -509,8 +505,6 @@
"check:window-globals-released": "a component that puts something on window must take it back. Heap snapshots (leak-722ac7789) measured SR+APG mounted twice at 54.2 -> 65.1 MB with live Vue apps 6 -> 8: every unmounted app stayed in memory, each pinned by one window key holding a closure, which keeps the component, its shadow root and the whole app. Flags only values that RETAIN (closures and object references), not warn-once latches or page-wide caches keyed off themselves. NOT in gates:pre: 19 sites remain across other owners' plugins. Run it directly.",
"check:describe-group": "the prayer packet's generated profile sentence (library/packets/describeGroup.js): each GSEC level states its own fact, and an index-ordered language name (\"Arabic, Moroccan\") is un-inverted by library/utils/languageLabel.js without shredding a comma inside parentheses, the 13 names the old split printed reversed. A catalogue arm renders every GSEC level from the REAL packet-strings.json in en and ar and fails on a blank sentence. Headless; in gates:pre.",
"check:descriptions": "every shipped profile must say what it is (reads the BUILT manifest)",
"check:dev-leaf-escape": "a dev leaf URL cannot write markup into the page it opens. The dev server's /__leaf/<bundle>/<profile>[/<instance>] route (and a production embed URL rewritten onto it) writes the URL's parts into the page's <title> and the element's profile-config attribute; it refused only .. and /, so a quote or a tag broke out of the attribute on a publicly reachable dev server. internal/plugin-gallery/dev-plugin.mjs refuses any part outside LEAF_PART and escapes the <title>. Drives the plugin's own middleware (no server): every leaf the dev manifest advertises is served in both URL forms; 12 hostile parts are refused in every position; escHtml escapes & < > \" '; and behind the OLD rule the <title> still escapes. Bodies are never printed (a leaf page carries the token). Sub-5s.",
"check:dev-leaf-escape:self-test": "4 poisons must each make check:dev-leaf-escape fail: the old guard (.. and / only), a part rule too strict for a real profile, an unescaped <title>, an escHtml that misses the quote.",
"check:element-census-vs-live": "what is on ONE side and not the other, ours vs the live doxa.life page we clone: harvests every visible TEXT NODE through every shadow root on both sides and diffs the SETS. OURS-ONLY unaccounted = a scope violation (FAIL), THEIRS-ONLY = a clone omission (FAIL), SANCTIONED = an allowlist entry carrying the ruling + date + card (refused without them), DATA DRIFT = same label, different digits (reported). The negative control is MANDATORY: a known ours-only string must land in the ours-only set or it prints CENSUS IS BLIND and exits 75; an unreachable live page is also 75. Presets: internal/element-census/<name>.json (--preset <name>, default all); ad hoc: --leaf <bundle/profile/index.html[#hash]> --live <url> --neg <text> [--sanctioned <file>]. Promoted from regions' probe; regions passes (list 229 vs 228, the only difference its sanctioned poster toggle). Through browser-slot.sh.",
"check:embed-base": "the shared URL resolver, pinned by ARITHMETIC — no browser, no network, no served tree, so it cannot SKIP itself into irrelevance. It computes what a leaf's own \"../../assets\" means and asserts the resolver agrees. Caught nothing for fifteen ticks because it did not exist: resolveAssetsBase computed one segment too high, every local gate passed (the dev web-root has assets BOTH inside and beside the tree), and the byte-level publish gates went green on the broken publish.",
"check:embed-basemap": "the gospel map's BASEMAP must paint inside a genuinely cross-origin iframe on the token the page BAKES, not one a probe forces on it. Every other browser gate here forces a restricted token (u=doxalife) that 403s on the basemap, then counts PINS — which come from our own GeoJSON and paint with no token at all — so 'the map renders' had never been measured. Runs a negative control every time by rewriting the token on the wire; if the control passes it reports PROVED NOTHING rather than a pass. ~60s, needs a browser.",
@ -537,8 +531,6 @@
"check:gallery-embed:live-cors": "the Research Gallery on a foreign page under the LIVE static layer's CORS rule (Access-Control-Allow-Origin on files inside the tree's folders, NOT on the root manifest.json, measured on the live test host): it must still boot every tab, by reading the CORS-readable manifest copy generate-tree writes beside its bundle.",
"check:gated-not-in-tree": "a GATED profile (the build config's gatedProfiles, today key-cities) never reaches the public tree: no public/<tree>/<name>/ folder, no manifest.json entry, no page referencing <name>/<name>.js. vite.config.js builds a gated bundle into .gated/<tree>/ (ignored by git). Data tables are out of scope on purpose: key-cities.json also feeds the production Cities with Internet map. Two-sided: three planted leaks in a temporary tree, each caught. Node only.",
"check:gospel-all": "EVERYTHING gospel-resources owns, in one command. Its ten gates were all hand-run, which is how a dead bundle shipped: a duplicate module killed all five leaves and the one gate run that day was pointed at the map. check:no-orphan-gates was right that they were reachable — reachable is not run. Deliberately NOT in gates:pre/post: three need a browser, the shared wave holds browser gates under load, and a bundle owner should not be able to slow everyone else's build by adding to a list they do not own. Static gates run first and stop the run on failure, so a scarce browser slot is never spent confirming a table is inconsistent. Exit 75 = NOT MEASURED, never folded into a pass.",
"check:gospel-clear-filter-url": "the gospel tables' Show all drops ONLY its own filter keys (iso, country, label, province): the page's ?tab=, a host's or partner's keys, the #hash and the history state stay. It used to replaceState the bare pathname, wiping a partner's query. Proves the pure helper (filter-url.js) and that data-tables.vue calls it; --poison runs the old wipe and must fail.",
"check:gospel-template-translators": "every translator a gospel-resources template calls (t, tr, T, tShared, tStatic) is a TOP-LEVEL binding of its <script setup>. data-tables.vue called t(...) while its translator is tr, so the tables page threw _ctx.t is not a function on every load and its loading line never drew. Static, node only; --poison restores the old call and must fail.",
"check:gospel-internet": "the Gospel x Internet board, headless, against the REAL table and the REAL kit bus: the join table is what its builder makes from main (2,107 / 2,107, province placement agreeing with gospel-by-province), every facet count equals the rows the bus then keeps (the five-numbers-agree invariant), D1 OR-within-key, the country->province cascade, the 369 zero-access headline by band 220/45/45/59, URL state round-trip, CSV header-first/provenance-last with injection neutralised, no engagement column; then every catalogue key the bundle names exists in all 11 locales and is translated. Each half has a --poison arm that must fail. Plugin-local because check:i18n-keys-exist cannot see a useOptionalT-based alias.",
"check:gospel-whiteboard": "the gospel set charts' geometry once drawn on a map: the bounded set's gap survives projection, the drawing stays inside the Mercator clamp, and three more properties, each with the arm that would catch its own defect. By the gospel owner. Headless.",
"check:hardcoded-english": "English a reader sees that no locale can reach: template text, static title=, script-block sentences, template literals, and quoted literals inside BOUND attributes, across the production plugins. Quiet on translator calls (including discovered aliases and proxy-t misses), te()-guarded fallbacks, css, urls and developer error text. Deliberately NOT in gates:post yet: it reports 179 today (gospel-resources 148, my-upg-100-list 25, cities-internet 6 incl. one cross-area key), with English inside dir=ltr islands listed as deliberate and not counted; it joins when that reaches 0 or every hit is carded. The :self-test (28 assertions, fixtures validated first so a malformed sample cannot pass as silence) runs in gates:pre.",

View file

@ -32,7 +32,6 @@
*/
import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue'
import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pane-wrapper.js'
import { urlOwnerOf, ownAddress } from '@map/composables/useViewAddress.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import ViewSelect from '@map/components/ViewSelect.vue'
import { countryName } from '@map/utils/countryName.js'
@ -105,11 +104,6 @@ const view = computed({
})
let column = null
let offScroll = null
let offOwn = null
// The page address (useViewAddress): ?pane= is the open view ('map'; the first pane is the default and never written).
// On this composite's own leaf it owns the address (ownAddress); nested in a shell it announces and the shell writes.
// The India pane exists only in an opted-in preview, so ?pane=india names nothing anywhere else.
const owner = () => urlOwnerOf(cfg.value)
// PAGE-LEVEL config keys a pane INHERITS from the host page. A cap-4 composite
// nests children as custom elements in THIS document, so anything the generator
@ -215,9 +209,8 @@ function onShowOnMap(e) {
onMounted(() => {
if (!stage.value) return
column = buildPaneColumn(spec.value, { host: stage.value, address: { owner } })
column = buildPaneColumn(spec.value, { host: stage.value })
if (!column) return
offOwn = ownAddress(stage.value, owner)
for (const pane of column.panes) {
if (!pane.config) continue
// ★ KEEP THE ELEMENT. It used to be created and dropped on the same line, so there was no
@ -243,7 +236,6 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener(SHOW_ON_MAP, onShowOnMap)
offScroll?.()
offOwn?.()
column?.destroy()
column = null
})

View file

@ -27,8 +27,6 @@ import { EN_MESSAGES } from '@map/i18n/index.js'
import DataTable from '@map/components/tables/DataTable.vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
import { commitUrl, urlOwnerOf } from '@map/composables/useViewAddress.js'
import { searchWithout, FILTER_KEYS } from './filter-url.js'
const profileConfig = inject('profileConfig', ref(null))
@ -150,12 +148,9 @@ const fLabel = ref((params.get('label') || '').trim())
const fProvince = ref((params.get('province') || '').trim())
const fold = (v) => String(v || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim()
const hasFilter = computed(() => !!(fIso.value || fCountry.value || fProvince.value))
// "Show all" drops only the filter's own keys (filter-url.js) through the one history writer, and only on a page
// this element owns (urlOwner 'app'); a partner's or a host's address is never written. The tab, every other key,
// the hash and the history state stay.
function clearFilter() {
fIso.value = ''; fCountry.value = ''; fLabel.value = ''; fProvince.value = ''
if (urlOwnerOf(profileConfig.value) === 'app') commitUrl({ search: searchWithout(window.location.search, FILTER_KEYS) })
try { window.history.replaceState({}, '', window.location.pathname) } catch (_) {}
}
const active = ref(TABS.some((t) => t.id === params.get('tab')) ? params.get('tab') : 'links')
@ -458,7 +453,7 @@ useShadowStyles(
</div>
<div v-if="loadErr" class="gt-err">{{ loadErr }}</div>
<div v-else-if="!table" class="gt-load">{{ tr('gospel.sheet.loadingTable', { table: tabLabel(current) }) }}</div>
<div v-else-if="!table" class="gt-load">{{ t('gospel.sheet.loadingTable', { table: current.label }) }}</div>
<DataTable
v-else
:key="current.id + ':' + fIso + ':' + fCountry + ':' + fProvince"

View file

@ -1,16 +0,0 @@
/**
* filter-url.js — the query after this page drops its OWN filter keys.
*
* The tables page reads a deep-link filter from the query (?iso= / ?country= / ?label= / ?province=).
* "Show all" must remove exactly those keys: the page's ?tab=, any key a host or a partner put on the
* address, and the #hash are someone else's and stay as they were. The result is in commitUrl's `search`
* shape ('' or '?…'), and every other parameter is kept as written, byte for byte. Pure (no DOM), so a node
* check can prove it without a browser.
*/
export const FILTER_KEYS = ['iso', 'country', 'label', 'province']
/** `search` ('' or '?…') without the parameters named in `keys`, in the same shape. */
export function searchWithout(search, keys = FILTER_KEYS) {
const kept = String(search || '').replace(/^\?/, '').split('&').filter((p) => p && !keys.includes(p.split('=')[0]))
return kept.length ? '?' + kept.join('&') : ''
}

View file

@ -1,53 +0,0 @@
#!/usr/bin/env node
/**
* check-clear-filter-url.mjs — "Show all" on the gospel tables drops ONLY the filter's own keys.
*
* WHY: clearFilter used to replaceState({}, '', pathname), which wiped the whole query and the hash —
* the page's own ?tab=, and any key a host or a partner had put on the address — and the history
* state with them. The address after "Show all" must differ from the one before by the filter keys
* alone.
*
* Proves it two ways: the pure helper (filter-url.js) on real-shaped addresses, and the component —
* data-tables.vue's clearFilter writes through commitUrl (the one history writer, which keeps the hash and the
* state) only on a page it owns, with no direct history write left.
* `--poison` runs the old behaviour through the same assertions and requires them to FAIL.
* Exit 0 pass · 1 fail.
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { searchWithout, FILTER_KEYS } from '../filter-url.js'
const HERE = dirname(fileURLToPath(import.meta.url))
const POISON = process.argv.includes('--poison')
// The address after "Show all", as commitUrl writes it ({ search } given, the current hash kept). Poison: the old wipe.
const strip = POISON ? (href) => new URL(href).pathname : (href) => { const u = new URL(href); return u.pathname + searchWithout(u.search, FILTER_KEYS) + u.hash }
let fails = 0
const ok = (c, m) => { if (c) console.log('ok ' + m); else { fails++; console.log('FAIL ' + m) } }
const base = 'https://example.org/dx-maps/gospel-resources/data-tables/index.html'
const a = strip(`${base}?tab=links&iso=hin&country=IND&label=Hindi&province=Bihar&partner=abc#tables`)
ok(!/[?&](iso|country|label|province)=/.test(a), `the filter keys are gone (${a})`)
ok(/[?&]tab=links\b/.test(a), 'the page\'s own ?tab= stays')
ok(/[?&]partner=abc\b/.test(a), 'a host\'s or partner\'s key stays')
ok(a.endsWith('#tables'), 'the #hash stays')
const b = strip(`${base}?iso=hin`)
ok(b === new URL(base).pathname, `only filter keys: no dangling "?" (${b})`)
const c = strip(`${base}?tab=countries`)
ok(c === new URL(base).pathname + '?tab=countries', 'no filter: the address is unchanged')
const d = strip(`${base}?tab=links&q=a%20b+c&iso=hin`)
ok(d.endsWith('?tab=links&q=a%20b+c'), `other parameters are kept as written, byte for byte (${d})`)
if (!POISON) {
const vue = readFileSync(resolve(HERE, '../data-tables.vue'), 'utf8')
const fn = (vue.match(/function clearFilter\(\)\s*\{[\s\S]*?\n\}/) || [''])[0]
ok(/import \{[^}]*\bcommitUrl\b[^}]*\burlOwnerOf\b[^}]*\} from '@map\/composables\/useViewAddress\.js'/.test(vue), 'data-tables.vue imports the one history writer (commitUrl) and urlOwnerOf')
ok(/commitUrl\(\{\s*search:\s*searchWithout\(window\.location\.search,\s*FILTER_KEYS\)\s*\}\)/.test(fn), 'clearFilter writes through commitUrl with only the filter keys dropped (the hash and state kept by commitUrl)')
ok(/urlOwnerOf\(profileConfig\.value\)\s*===\s*'app'/.test(fn), 'and only on a page it owns (urlOwner app): a partner\'s address is never written')
ok(!/\.(push|replace)State\s*\(/.test(vue), 'no direct history write is left in data-tables.vue')
}
if (POISON) { console.log(fails ? `POISON OK: ${fails} caught` : 'POISON FAILED'); process.exit(fails ? 0 : 1) }
console.log(fails ? `FAIL check-clear-filter-url: ${fails}` : 'PASS check-clear-filter-url')
process.exit(fails ? 1 : 0)

View file

@ -1,47 +0,0 @@
#!/usr/bin/env node
/**
* check-template-translators.mjs — every translator a gospel-resources template calls is bound in its <script setup>.
*
* WHY: data-tables.vue's template called t(...) while its translator is `tr`, and no top-level `t` existed. A
* <script setup> template resolves an unbound name through the component proxy (_ctx.t), so the tables page threw
* "_ctx.t is not a function" (minified "I.t") on every load, the loading line never drew, and nothing failed a gate.
*
* For each .vue in plugin-designs/gospel-resources: the translator-shaped calls in its template (t, tr, T, tShared,
* tStatic) must each be a TOP-LEVEL binding of its <script setup> (const / let / function / import), not a name
* that only appears inside a function. `--poison` runs the same check on the old data-tables.vue line and must fail.
* Exit 0 pass · 1 fail.
*/
import { readFileSync, readdirSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const POISON = process.argv.includes('--poison')
const NAMES = ['t', 'tr', 'T', 'tShared', 'tStatic']
function unbound(src) {
const tpl = (src.match(/^<template>([\s\S]*)^<\/template>/m) || [])[1] || ''
const script = (src.match(/^<script setup[^>]*>([\s\S]*?)^<\/script>/m) || [])[1] || ''
// TOP-LEVEL bindings: declarations at column 0 of the script block, and imported names.
const top = new Set()
for (const m of script.matchAll(/^(?:const|let|var|function)\s+([A-Za-z_$][\w$]*)/gm)) top.add(m[1])
for (const m of script.matchAll(/^(?:const|let|var)\s+\{([^}]*)\}\s*=/gm)) for (const n of m[1].split(',')) top.add(n.split(':').pop().trim())
for (const m of script.matchAll(/^import\s+\{([^}]*)\}/gm)) for (const n of m[1].split(',')) top.add(n.split(/\s+as\s+/).pop().trim())
const called = new Set([...tpl.matchAll(/(?:^|[^\w$.])([A-Za-z_$][\w$]*)\(/g)].map((m) => m[1]).filter((n) => NAMES.includes(n)))
return [...called].filter((n) => !top.has(n))
}
let fails = 0
if (POISON) {
const src = readFileSync(join(DIR, 'data-tables.vue'), 'utf8').replace("{{ tr('gospel.sheet.loadingTable'", "{{ t('gospel.sheet.loadingTable'")
const u = unbound(src)
console.log(u.includes('t') ? 'POISON OK: the old data-tables.vue call t(...) is caught as unbound' : 'POISON FAILED')
process.exit(u.includes('t') ? 0 : 1)
}
for (const f of readdirSync(DIR).filter((f) => f.endsWith('.vue'))) {
const u = unbound(readFileSync(join(DIR, f), 'utf8'))
if (u.length) { fails++; console.log(`FAIL ${f}: the template calls ${u.map((n) => n + '(…)').join(', ')} but its <script setup> binds no such name at the top level`) }
else console.log(`ok ${f}`)
}
console.log(fails ? `FAIL check-template-translators: ${fails}` : 'PASS check-template-translators')
process.exit(fails ? 1 : 0)

View file

@ -36,7 +36,6 @@
import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue'
import { useOptionalT } from '@map/composables/useOptionalT.js'
import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pane-wrapper.js'
import { urlOwnerOf, ownAddress } from '@map/composables/useViewAddress.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import ViewSelect from '@map/components/ViewSelect.vue'
import ShareButton from '@map/components/map-controls/ShareButton.vue'
@ -72,8 +71,6 @@ import MapInfoButton from '@map/components/map-controls/MapInfoButton.vue'
const t = useOptionalT('')
const cfg = inject('profileConfig', ref(null))
// P2: the pane in view is part of the address (?pane=), written by whoever owns this page (urlOwnerOf).
const owner = () => urlOwnerOf(cfg.value)
const tk = inject('mapboxToken', ref(''))
const uiStore = inject('uiStore', null)
const isDark = computed(() => !!uiStore?.isDark)
@ -153,7 +150,6 @@ const view = computed({
})
let column = null
let offScroll = null
let offOwn = null // P2: ownAddress's undo, called on unmount
// The mounted custom element per pane id. Kept because a cross-pane action has to reach the
// TARGET PANE'S ELEMENT: upg-100-list listens for 'doxa:select-zone' on itself rather than on
// window, precisely so two embeds on one page do not cross-talk — which means the shell has to
@ -304,9 +300,8 @@ function onShowOnMap(e) {
onMounted(() => {
if (!stage.value) return
column = buildPaneColumn(spec.value, { host: stage.value, address: { owner } })
column = buildPaneColumn(spec.value, { host: stage.value })
if (!column) return
offOwn = ownAddress(stage.value, owner)
for (const pane of column.panes) {
if (!pane.config) continue
const el = paneElement(pane)
@ -335,8 +330,6 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener('doxa-upg-100-list:show-on-map', onShowOnMap)
offScroll?.()
offOwn?.()
offOwn = null
column?.destroy()
column = null
// Drop the element references with the column that owned them — holding a detached custom

View file

@ -225,7 +225,7 @@ import StudioTree from './src/StudioTree.vue'
import { LANGUAGES, loadLocale, saveLocale, getLanguage, normaliseLocale } from '@map/layouts/app-wrapper-material/languages.js'
import { publishMapToken, clearMapToken } from '@map/composables/useSharedMapToken.js'
import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js'
import { readViewKeys, writeViewKeys, onAddressChange, urlOwnerOf, commitUrl, ownAddress } from '@map/composables/useViewAddress.js'
import { readViewKeys, writeViewKeys, onAddressChange, urlOwnerOf, commitUrl } from '@map/composables/useViewAddress.js'
// The reader's own map token / token-service URL. localStorage only: never written into the build,
// never sent anywhere but Mapbox (the token) or the URL the reader typed (the service).
@ -769,9 +769,6 @@ export default defineComponent({
if (key) this.select(key, { fromHistory: true })
else if (this.rows.length) this.select(this.rows[0].key, { fromHistory: true })
})
// A nested composite's own choice (its open pane, its place) is announced up to here and written beside ?tab=,
// while this page owns its address. Only pane and place: the tab is Studio's own.
this._offOwn = ownAddress(this.$el, () => urlOwnerOf(this.cfg))
},
// The corner-widget hosts live on window.__doxaStudioWidgets so a HOT UPDATE remounts Studio onto
// the same hosts and a live widget (Rudder) keeps its state. A real unmount must release them, or
@ -780,7 +777,6 @@ export default defineComponent({
// FIRST, before the hot-update return below: a remounted Studio adds its own listener, and a kept one would
// re-apply every Back twice.
if (this._offAddress) { this._offAddress(); this._offAddress = null }
if (this._offOwn) { this._offOwn(); this._offOwn = null }
if (import.meta.hot) return
const W = window.__doxaStudioWidgets
if (W && W.hosts) for (const h of Object.values(W.hosts)) { try { h.remove() } catch (_) {} }

View file

@ -74,7 +74,7 @@ import BackToTopButton from '@map/components/BackToTopButton.vue'
import ShellLanguageMenu from '@map/components/ShellLanguageMenu.vue'
import { saveLocale } from '@map/layouts/app-wrapper-material/languages.js'
import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js'
import { readViewKeys, writeViewKeys, onAddressChange, urlOwnerOf, ownAddress } from '@map/composables/useViewAddress.js'
import { readViewKeys, writeViewKeys, onAddressChange, urlOwnerOf } from '@map/composables/useViewAddress.js'
import { translate, LOCALES } from './src/strings.js'
import { SECTIONS, TABS, resolveTabs } from './src/tabs.js'
@ -233,9 +233,6 @@ export default defineComponent({
const want = this.requestedTab()
const known = !want || this.tabs.some((t) => t.id === want)
this.select(this.initialIndex(), { firstPaint: true, fromHistory: !known })
// A nested plugin's choice inside a tab (?pane=, ?place=) reaches the address through the owner: recorded
// only while this page owns its address, never a tab switch, and the event stops here.
this._offOwn = ownAddress(this.$el, () => urlOwnerOf(this.cfg))
// Back / Forward re-apply the tab from the address and never write (writing would lose Forward).
this._offAddress = onAddressChange(({ tab }) => {
const i = this.tabs.findIndex((t) => t.id === tab)
@ -246,7 +243,6 @@ export default defineComponent({
beforeUnmount() {
if (this._langObs) { this._langObs.disconnect(); this._langObs = null }
if (this._offAddress) { this._offAddress(); this._offAddress = null }
if (this._offOwn) { this._offOwn(); this._offOwn = null }
},
methods: {
tr(key, params) { return translate(this.uiLocale, key, params) },
@ -281,9 +277,7 @@ export default defineComponent({
// 2. <tree>/doxa-maps-spa/manifest.json, the byte-identical copy generate-tree writes for the
// Research Gallery. The live static layer sends CORS headers only inside a tree folder, not
// on the root manifest, so a partner page's script embed reads this copy FIRST;
// 3. /<tree>/manifest.json, the built tree as the dev server serves it (at the ROOT). NOT /public/<tree>/:
// the dev server treats a /public/… request as SOURCE and injects an import into each built IIFE, which
// then fails as a classic script ("Cannot use import statement outside a module").
// 3. /public/<tree>/manifest.json, where the dev server keeps the built manifest.
// Script URLs always resolve against the tree ROOT, never the copy's folder.
loadManifest() {
const dirOf = (u) => { try { return new URL(u, location.href).href.replace(/[^/]*$/, '') } catch { return '' } }
@ -293,7 +287,7 @@ export default defineComponent({
let crossOrigin = false
try { crossOrigin = new URL(rootUrl, location.href).origin !== location.origin } catch { /* relative = same */ }
const candidates = crossOrigin ? [copy, root] : [root, copy]
const devFallback = '/' + __DOXA_TREE_NAME__ + '/manifest.json'
const devFallback = '/public/' + __DOXA_TREE_NAME__ + '/manifest.json'
if (!candidates.some((c) => c.url === devFallback)) candidates.push({ url: devFallback, root: dirOf(devFallback) })
const tried = []
const attempt = (i) => {

View file

@ -77,7 +77,7 @@ const GLOBALS = [
'DataView', 'WeakRef', 'FinalizationRegistry', 'ClipboardItem', 'CSSStyleSheet', 'XMLSerializer',
// Build-time defines: vite REPLACES these before the browser ever sees them (vite.config.js
// `define`), so they are bound by the bundler rather than by the file.
'__DOXA_TREE_NAME__', '__DOXA_QA__', '__DOXA_SHARE_BASE__',
'__DOXA_TREE_NAME__', '__DOXA_QA__',
// Bound by the build too, but by a transform rather than `define`: with DX_SHIP_ONE_LOCALE the
// i18nAreasPlugin prepends `const __DX_LOCALE_SRI__ = {…}` to library/i18n/index.js; without it the
// name does not exist, which is why its one read sits behind `typeof … !== 'undefined'`.

File diff suppressed because one or more lines are too long

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
<script src="../all-people-groups.js" integrity="sha384-4uZxLJxLey7Zl7tiZ5qTwrQnblpRX0lxzKzjTcfUqBNEOP2Qn63Bvtra+Vo+6ecm" crossorigin="anonymous"></script>
<script src="../all-people-groups.js" integrity="sha384-XZupcGRo5bafMWWJsYcOJVOhT4Mw4Vv/v4hgVKMrQU5X8IoRLP11Z9PoGkbxbxLS" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -52,7 +52,7 @@
"n": 1,
"rhythm": "praise",
"sub": "Two-Cross Gospel",
"text": "Praise Jesus for taking up a cross for you and being humbled. Crown Him as your King, and pray for these nations (people groups) that they will do the same. Devote yourself to Jesus to take up your own cross and follow Him, even through suffering, and death. Praise Him for He is worthy of these nations (people groups) taking up their own cross, willing to follow Jesus no matter what the cost. May the Lamb receive the reward of His suffering, starting with me.",
"text": "That they accept Christ as King, forgiven by His cross — and take up their own cross, willing to suffer through persecution. May the Lamb receive the reward of His suffering, starting with me.",
"refs": [
"Matthew 27:27",
"Matthew 16:24–25"
@ -62,7 +62,7 @@
"n": 2,
"rhythm": "wait",
"sub": "Holy Spirit & deliverance",
"text": "Spend time waiting on the Holy Spirit to fill you and empower you to pray as a bold prayer warrior for them. Focus in on God's presence, and wait on the Lord!",
"text": "Spend time waiting on the Holy Spirit to tell you and empower you to pray as a bold prayer warrior for them: that they are filled with the Holy Spirit and made bold witnesses, not isolated; baptized in water and Spirit; every evil grip broken; and that believers in neighboring people groups are mobilized as laborers.",
"refs": [
"Acts 1:8",
"John 14:26"
@ -72,7 +72,7 @@
"n": 3,
"rhythm": "confess",
"sub": "",
"text": "Be open and honest with God about your fears, lies, and unbelief surrounding unreached people groups (Example lie: It is too big an ask for God to reach all of these unreached people groups in our lifetime). Receive His love, truth, and voice surrounding unreached people groups and your role in helping Him reach these people groups. Ask God to revive your love and faith for the people groups on your prayer list.",
"text": "We confess the unbelief that it is too big an ask for God to reach all of these unreached people groups in our lifetime. We receive the truth that He is reviving our love and faith for the global unreached.",
"refs": [
"John 3:16",
"1 John 1:9"
@ -81,8 +81,8 @@
{
"n": 4,
"rhythm": "read",
"sub": "the heavenly Father's love",
"text": "That they would be SAVED and KNOW their heavenly Father loves them — He wants to save them, and through them He wants to bring salvation to their family, friends, neighbors, coworkers, classmates, everyone they know personally.",
"sub": "the Father's love",
"text": "That they would be SAVED and KNOW their heavenly Father loves them — and that through them He would bring salvation to their family, friends, neighbours, coworkers, classmates, everyone they know personally.",
"refs": [
"Mark 5:18–20",
"Acts 16:31"
@ -93,7 +93,7 @@
"n": 5,
"rhythm": "ask",
"sub": "baptisms",
"text": "New baptisms among the indigenous people groups, and every baptized disciple is trained to baptize others — multiplying baptisms.",
"text": "New baptisms among the indigenous people groups, and every baptized disciple trained to baptize others — multiplying baptisms.",
"refs": [
"Acts 16",
"Acts 10"
@ -103,7 +103,7 @@
"n": 6,
"rhythm": "intercede",
"sub": "laborers & shepherds",
"text": "For laborers sent to deeply love a people they have never met; and indigenous good shepherds raised up from within the people groups.",
"text": "Laborers sent to deeply love a people they have never met; good shepherds raised up from within.",
"refs": [
"Luke 10:2",
"John 10:11–15"
@ -113,7 +113,7 @@
"n": 7,
"rhythm": "pray_word",
"sub": "Persons & Houses of Peace",
"text": "That laborers and indigenous disciples find the receptive person and the house of peace, deeply love them, disciple them well, baptize them, and train them to reach their friends and family with their newfound love for Jesus.",
"text": "That laborers and indigenous leaders find the receptive person and the house of peace, deeply love them, disciple them well, baptize them, and train them to reach their friends and family with their newfound love for Jesus.",
"refs": [
"Luke 10:5–7",
"Matthew 10:5–14"
@ -133,17 +133,17 @@
"n": 9,
"rhythm": "sing",
"sub": "",
"text": "Imagine you are with Jesus worshipping Him, and you are surrounded by all the people groups on your prayer list worshipping Him too. Spend some time to praise Jesus.",
"text": "That the people group is flooded with worshippers — worship everywhere, in freedom and in chains.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
"n": 10,
"rhythm": "meditate",
"sub": "communion",
"text": "Focus on Jesus. Take communion if possible. Imagine being together with Jesus at a table with one person from each people group on your list. What is it going to take for this to become a reality?",
"text": "That they take communion together and grasp the freedom they have in Christ through His death, burial and resurrection.",
"refs": [
"Matthew 26:26–30",
"1 Corinthians 11:23–26"
@ -163,10 +163,11 @@
"n": 12,
"rhythm": "praise_end",
"sub": "blessing · great · greater · greatest",
"text": "Short reading: It is a blessing from God for people in these people groups to be saved through Jesus and have eternal life (John 17:3). It is a great blessing from God for indigenous disciples to lead their friends, family, coworkers, classmates, neighbors, and people they don't know, to Jesus through their witness (John 17:20). It is a greater blessing from God when indigenous leaders in these people groups start new churches that are unified in Jesus (John 17:21). It is one of the greatest blessings from God, when indigenous leaders train other indigenous leaders to start more churches unified in Christ. They begin to worship Jesus and spread throughout neighboring unreached people groups bringing the life of Jesus to the ends of the earth. Spend time singing praise to God. He is worthy of worshippers in all these people groups!",
"text": "Pray Jesus’ prayer in John 17 over them. The blessing: eternal life, knowing God and Jesus, whom He sent. The great blessing: their own believers leading others to faith. The greater blessing: new spiritual families of their own, one in Christ. One of the greatest: leaders from among them who train others to start new spiritual families, again and again. Ask all this and more, beyond all we ask or think. Ask, as Jesus did, that they be one and kept from the evil one. None of it is possible unless Jesus is who He says He is.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -174,8 +175,8 @@
"point4_passage_credit": "Mark 5:18–20 · World English Bible (public domain)",
"point4_passage_bold": "He didn’t allow him, but said to him, “Go to your house, to your friends, and tell them what great things the Lord has done for you and how he had mercy on you.”",
"point4_heart_title": "THE HEAVENLY FATHER'S HEART",
"point4_heart_lede": "They need to know that their heavenly Father loves them, He wants to save them, and through them He wants their unreached people group neighbors to be saved.",
"point4_heart_leadin": "More scriptures about the heavenly Father's heart (pick one scripture to read)",
"point4_heart_lede": "They need to know that their Heavenly Father loves them, He wants to save them, and through them He wants their unreached people group neighbors to be saved.",
"point4_heart_leadin": "More scriptures about the heavenly Father's heart",
"point4_heart": [
{
"who": "Noah",
@ -262,7 +263,6 @@
"sets_bounded_title": "Reached, and not",
"sets_centered_title": "Distance from a neighbour",
"sets_bounded_tally": "{inside} inside · {outside} outside · {unknown} unknown",
"sets_no_data": "{n} without data",
"sets_centered_tally": "{placed} placed · {centre} at the centre · {unplaced} unplaced",
"ring_band_dialect": "Same dialect",
"ring_band_language": "Same language",
@ -297,8 +297,7 @@
"Haggai": "Haggai",
"Isaiah": "Isaiah",
"Habakkuk": "Habakkuk",
"Ephesians": "Ephesians",
"Revelation": "Revelation"
"Ephesians": "Ephesians"
},
"christian_per_cell": "Each box ≈ {n} people",
"howto_title": "How to pray with this list",
@ -489,7 +488,7 @@
"text": "Que el grupo étnico sea inundado de adoradores — adoración en todas partes, en libertad y en cadenas.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -518,8 +517,9 @@
"sub": "bendición · grande · mayor · de las mayores",
"text": "Ora sobre ellos la oración de Jesús en Juan 17. La bendición: la vida eterna, conocer a Dios y a Jesús, a quien él envió. La gran bendición: que sus propios creyentes lleven a otros a la fe. La bendición mayor: nuevas familias espirituales propias, unidas en Cristo. Una de las mayores: líderes de entre ellos que formen a otros para iniciar nuevas familias espirituales, una y otra vez. Pide todo esto y más, mucho más de lo que pedimos o pensamos. Pide, como Jesús, que sean uno y que sean guardados del maligno. Nada de esto es posible si Jesús no es quien dice ser.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -608,7 +608,6 @@
"sets_bounded_title": "Alcanzados y no alcanzados",
"sets_centered_title": "Distancia a un vecino",
"sets_bounded_tally": "{inside} dentro · {outside} fuera · {unknown} desconocido",
"sets_no_data": "{n} sin datos",
"sets_centered_tally": "{placed} ubicados · {centre} en el centro · {unplaced} sin ubicar",
"ring_band_dialect": "Mismo dialecto",
"ring_band_language": "Misma lengua",
@ -643,8 +642,7 @@
"Haggai": "Hageo",
"Isaiah": "Isaías",
"Habakkuk": "Habacuc",
"Ephesians": "Efesios",
"Revelation": "Apocalipsis"
"Ephesians": "Efesios"
},
"christian_per_cell": "Cada casilla ≈ {n} personas",
"howto_title": "Cómo orar con esta lista",
@ -821,7 +819,7 @@
"text": "Que le groupe ethnique soit inondé d'adorateurs — l'adoration partout, dans la liberté comme dans les chaînes.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -850,8 +848,9 @@
"sub": "bénédiction · grande · plus grande · parmi les plus grandes",
"text": "Priez sur eux la prière de Jésus en Jean 17. La bénédiction : la vie éternelle, connaître Dieu et Jésus, celui qu’il a envoyé. La grande bénédiction : que leurs propres croyants amènent d’autres à la foi. Une bénédiction plus grande : de nouvelles familles spirituelles bien à eux, unies en Christ. L’une des plus grandes : des responsables issus de leur peuple qui forment d’autres personnes à fonder de nouvelles familles spirituelles, encore et encore. Demandez tout cela et plus encore, au-delà de tout ce que nous demandons ou pensons. Demandez, comme Jésus, qu’ils soient un et gardés du malin. Rien de cela n’est possible si Jésus n’est pas celui qu’il dit être.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -940,7 +939,6 @@
"sets_bounded_title": "Atteints, ou non",
"sets_centered_title": "Distance d’un voisin",
"sets_bounded_tally": "{inside} à l’intérieur · {outside} à l’extérieur · {unknown} inconnu",
"sets_no_data": "{n} sans données",
"sets_centered_tally": "{placed} placés · {centre} au centre · {unplaced} non placés",
"ring_band_dialect": "Même dialecte",
"ring_band_language": "Même langue",
@ -975,8 +973,7 @@
"Haggai": "Aggée",
"Isaiah": "Ésaïe",
"Habakkuk": "Habacuc",
"Ephesians": "Éphésiens",
"Revelation": "Apocalypse"
"Ephesians": "Éphésiens"
},
"christian_per_cell": "Chaque case ≈ {n} personnes",
"howto_title": "Comment prier avec cette liste",
@ -1153,7 +1150,7 @@
"text": "Que o grupo étnico seja inundado de adoradores — adoração em toda parte, em liberdade e em correntes.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -1182,8 +1179,9 @@
"sub": "bênção · grande · maior · das maiores",
"text": "Ore sobre eles a oração de Jesus em João 17. A bênção: a vida eterna, conhecer a Deus e a Jesus, a quem ele enviou. A grande bênção: que os seus próprios crentes levem outros à fé. A bênção maior: novas famílias espirituais deles mesmos, unidas em Cristo. Uma das maiores: líderes do meio deles que preparem outros para começar novas famílias espirituais, vez após vez. Peça tudo isso e mais, muito além do que pedimos ou pensamos. Peça, como Jesus pediu, que sejam um e guardados do maligno. Nada disso é possível se Jesus não for quem ele diz ser.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -1269,7 +1267,6 @@
"sets_bounded_title": "Alcançados e não alcançados",
"sets_centered_title": "Distância de um vizinho",
"sets_bounded_tally": "{inside} dentro · {outside} fora · {unknown} desconhecido",
"sets_no_data": "{n} sem dados",
"sets_centered_tally": "{placed} posicionados · {centre} no centro · {unplaced} sem posição",
"ring_band_dialect": "Mesmo dialeto",
"ring_band_language": "Mesma língua",
@ -1304,8 +1301,7 @@
"Haggai": "Ageu",
"Isaiah": "Isaías",
"Habakkuk": "Habacuque",
"Ephesians": "Efésios",
"Revelation": "Apocalipse"
"Ephesians": "Efésios"
},
"christian_per_cell": "Cada caixa ≈ {n} pessoas",
"howto_title": "Como orar com esta lista",
@ -1482,7 +1478,7 @@
"text": "Dass die Volksgruppe von Anbetern überflutet wird — Anbetung überall, in Freiheit und in Ketten.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -1511,8 +1507,9 @@
"sub": "Segen · groß · größer · einer der größten",
"text": "Beten Sie Jesu Gebet aus Johannes 17 über ihnen. Der Segen: ewiges Leben, Gott zu kennen und Jesus, den er gesandt hat. Der große Segen: dass ihre eigenen Gläubigen andere zum Glauben führen. Der größere Segen: neue geistliche Familien, ihre eigenen, eins in Christus. Einer der größten: Leiter aus ihrer Mitte, die andere anleiten, neue geistliche Familien zu gründen, immer wieder. Bitten Sie um all das und mehr, über alles hinaus, was wir bitten oder verstehen. Bitten Sie wie Jesus, dass sie eins seien und vor dem Bösen bewahrt werden. Nichts davon ist möglich, wenn Jesus nicht der ist, der er zu sein sagt.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -1601,7 +1598,6 @@
"sets_bounded_title": "Erreicht und nicht erreicht",
"sets_centered_title": "Abstand zu einem Nachbarn",
"sets_bounded_tally": "{inside} innen · {outside} außen · {unknown} unbekannt",
"sets_no_data": "{n} ohne Daten",
"sets_centered_tally": "{placed} platziert · {centre} in der Mitte · {unplaced} ohne Platz",
"ring_band_dialect": "Gleicher Dialekt",
"ring_band_language": "Gleiche Sprache",
@ -1636,8 +1632,7 @@
"Haggai": "Haggai",
"Isaiah": "Jesaja",
"Habakkuk": "Habakuk",
"Ephesians": "Epheser",
"Revelation": "Offenbarung"
"Ephesians": "Epheser"
},
"christian_per_cell": "Jedes Kästchen ≈ {n} Menschen",
"howto_title": "So beten Sie mit dieser Liste",
@ -1814,7 +1809,7 @@
"text": "Che il gruppo etnico sia inondato di adoratori — adorazione ovunque, nella libertà e nelle catene.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -1843,8 +1838,9 @@
"sub": "benedizione · grande · più grande · tra le più grandi",
"text": "Prega su di loro la preghiera di Gesù in Giovanni 17. La benedizione: la vita eterna, conoscere Dio e Gesù, colui che egli ha mandato. La grande benedizione: che i loro stessi credenti portino altri alla fede. La benedizione più grande: nuove famiglie spirituali tutte loro, unite in Cristo. Una delle più grandi: responsabili del loro popolo che formano altri ad avviare nuove famiglie spirituali, ancora e ancora. Chiedi tutto questo e di più, oltre tutto ciò che chiediamo o pensiamo. Chiedi, come Gesù, che siano uno e custoditi dal maligno. Niente di tutto questo è possibile se Gesù non è chi dice di essere.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -1930,7 +1926,6 @@
"sets_bounded_title": "Raggiunti e non raggiunti",
"sets_centered_title": "Distanza da un vicino",
"sets_bounded_tally": "{inside} dentro · {outside} fuori · {unknown} sconosciuto",
"sets_no_data": "{n} senza dati",
"sets_centered_tally": "{placed} collocati · {centre} al centro · {unplaced} non collocati",
"ring_band_dialect": "Stesso dialetto",
"ring_band_language": "Stessa lingua",
@ -1965,8 +1960,7 @@
"Haggai": "Aggeo",
"Isaiah": "Isaia",
"Habakkuk": "Abacuc",
"Ephesians": "Efesini",
"Revelation": "Apocalisse"
"Ephesians": "Efesini"
},
"christian_per_cell": "Ogni casella ≈ {n} persone",
"howto_title": "Come pregare con questa lista",
@ -2143,7 +2137,7 @@
"text": "Grupul etnic să fie inundat de închinători — închinare pretutindeni, în libertate și în lanțuri.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -2172,8 +2166,9 @@
"sub": "binecuvântare · mare · mai mare · printre cele mai mari",
"text": "Roagă-te peste ei cu rugăciunea lui Isus din Ioan 17. Binecuvântarea: viața veșnică, să-L cunoască pe Dumnezeu și pe Isus, pe care L-a trimis. Marea binecuvântare: ca propriii lor credincioși să-i aducă pe alții la credință. O binecuvântare și mai mare: familii spirituale noi, ale lor, una în Hristos. Una dintre cele mai mari: lideri dintre ei care îi pregătesc pe alții să înceapă noi familii spirituale, iar și iar. Cere toate acestea și mai mult, dincolo de tot ce cerem sau gândim. Cere, ca Isus, să fie una și păziți de cel rău. Nimic din toate acestea nu este posibil dacă Isus nu este Cel care spune că este.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -2259,7 +2254,6 @@
"sets_bounded_title": "Atinse și neatinse",
"sets_centered_title": "Distanța față de un vecin",
"sets_bounded_tally": "{inside} înăuntru · {outside} în afară · {unknown} necunoscut",
"sets_no_data": "{n} fără date",
"sets_centered_tally": "{placed} plasate · {centre} în centru · {unplaced} neplasate",
"ring_band_dialect": "Același dialect",
"ring_band_language": "Aceeași limbă",
@ -2294,8 +2288,7 @@
"Haggai": "Hagai",
"Isaiah": "Isaia",
"Habakkuk": "Habacuc",
"Ephesians": "Efeseni",
"Revelation": "Apocalipsa"
"Ephesians": "Efeseni"
},
"christian_per_cell": "Fiecare căsuță ≈ {n} de oameni",
"howto_title": "Cum să te rogi cu această listă",
@ -2470,7 +2463,7 @@
"text": "Чтобы народ наполнился поклонниками — поклонение повсюду, в свободе и в цепях.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -2499,8 +2492,9 @@
"sub": "благословение · великое · большее · одно из величайших",
"text": "Молитесь над ними молитвой Иисуса из Иоанна 17. Благословение: вечная жизнь — знать Бога и Иисуса, Которого Он послал. Великое благословение: чтобы их собственные верующие приводили других к вере. Ещё большее благословение: новые духовные семьи, свои собственные, единые во Христе. Одно из величайших: лидеры из их среды, которые учат других начинать новые духовные семьи — снова и снова. Просите обо всём этом и о большем — превыше всего, чего мы просим или о чём помышляем. Просите, как Иисус, чтобы они были едины и сохранены от лукавого. Ничто из этого невозможно, если Иисус не Тот, Кем Он Себя называет.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -2589,7 +2583,6 @@
"sets_bounded_title": "Достигнутые и нет",
"sets_centered_title": "Расстояние до соседа",
"sets_bounded_tally": "{inside} внутри · {outside} снаружи · {unknown} неизвестно",
"sets_no_data": "{n} без данных",
"sets_centered_tally": "{placed} размещено · {centre} в центре · {unplaced} без места",
"ring_band_dialect": "Тот же диалект",
"ring_band_language": "Тот же язык",
@ -2624,8 +2617,7 @@
"Haggai": "Агг.",
"Isaiah": "Ис.",
"Habakkuk": "Авв.",
"Ephesians": "Еф.",
"Revelation": "Откр."
"Ephesians": "Еф."
},
"christian_per_cell": "Каждая клетка ≈ {n} чел.",
"howto_title": "Как молиться по этому списку",
@ -2809,7 +2801,7 @@
"text": "أن يفيض الشعب بالساجدين — عبادة في كل مكان، في الحرية وفي القيود.",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -2838,8 +2830,9 @@
"sub": "البركة · العظيمة · الأعظم · من أعظمها",
"text": "صلِّ لأجلهم بصلاة يسوع في يوحنا ١٧. البركة: الحياة الأبدية، أن يعرفوا الله ويسوع الذي أرسله. البركة العظيمة: أن يقود المؤمنون منهم غيرهم إلى الإيمان. البركة الأعظم: عائلات روحية جديدة خاصة بهم، واحدة في المسيح. ومن أعظم البركات: قادة من بينهم يدرّبون آخرين ليبدؤوا عائلات روحية جديدة، مرة بعد مرة. اطلب كل هذا وأكثر، أكثر جدًا مما نطلب أو نفتكر. واطلب، كما طلب يسوع، أن يكونوا واحدًا وأن يُحفظوا من الشرير. لا شيء من هذا ممكن إن لم يكن يسوع هو مَن يقول إنه هو.",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -2928,7 +2921,6 @@
"sets_bounded_title": "وصلتهم البشارة ومن لم تصلهم",
"sets_centered_title": "المسافة إلى جار",
"sets_bounded_tally": "{inside} في الداخل · {outside} في الخارج · {unknown} غير معروف",
"sets_no_data": "{n} بلا بيانات",
"sets_centered_tally": "{placed} موضوعون · {centre} في المركز · {unplaced} بلا موضع",
"ring_band_dialect": "اللهجة نفسها",
"ring_band_language": "اللغة نفسها",
@ -2963,8 +2955,7 @@
"Haggai": "حجي",
"Isaiah": "إشعياء",
"Habakkuk": "حبقوق",
"Ephesians": "أفسس",
"Revelation": "رؤيا"
"Ephesians": "أفسس"
},
"christian_per_cell": "كل مربّع ≈ {n} شخص",
"howto_title": "كيف تصلّي بهذه القائمة",
@ -3141,7 +3132,7 @@
"text": "कि वह जनसमूह आराधकों से भर जाए — हर जगह आराधना, स्वतंत्रता में और बेड़ियों में भी।",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -3170,8 +3161,9 @@
"sub": "आशीष · बड़ी · और बड़ी · सबसे बड़ी में से एक",
"text": "यूहन्ना 17 में यीशु ने जो प्रार्थना की, वही उनके लिए करें। आशीष: अनन्त जीवन — परमेश्वर को और यीशु को जानना, जिन्हें उन्होंने भेजा। बड़ी आशीष: उन्हीं के विश्वासी दूसरों को विश्वास में लाएँ। उससे बड़ी आशीष: उनके अपने नए आत्मिक परिवार, मसीह में एक। सबसे बड़ी आशीषों में से एक: उनके बीच से अगुवे, जो दूसरों को नए आत्मिक परिवार शुरू करने के लिए तैयार करें — बार-बार। यह सब और इससे भी अधिक माँगें, जितना हम माँगते या सोचते हैं उससे कहीं बढ़कर। यीशु की तरह माँगें कि वे एक हों और उस दुष्ट से बचे रहें। इनमें से कुछ भी संभव नहीं, यदि यीशु वह नहीं हैं जो वह कहते हैं कि वह हैं।",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -3257,7 +3249,6 @@
"sets_bounded_title": "पहुँचे और नहीं पहुँचे",
"sets_centered_title": "पड़ोसी से दूरी",
"sets_bounded_tally": "{inside} भीतर · {outside} बाहर · {unknown} अज्ञात",
"sets_no_data": "{n} बिना आँकड़ों के",
"sets_centered_tally": "{placed} स्थित · {centre} केंद्र में · {unplaced} अस्थित",
"ring_band_dialect": "वही बोली",
"ring_band_language": "वही भाषा",
@ -3292,8 +3283,7 @@
"Haggai": "हाग्गै",
"Isaiah": "यशायाह",
"Habakkuk": "हबक्कूक",
"Ephesians": "इफिसियों",
"Revelation": "प्रकाशितवाक्य"
"Ephesians": "इफिसियों"
},
"christian_per_cell": "हर खाना ≈ {n} लोग",
"howto_title": "इस सूची के साथ प्रार्थना कैसे करें",
@ -3468,7 +3458,7 @@
"text": "愿这族群充满敬拜者——处处敬拜,在自由中,也在捆锁中。",
"refs": [
"Acts 16:25–26",
"Revelation 7:9–10"
"Colossians 3:16"
]
},
{
@ -3497,8 +3487,9 @@
"sub": "祝福 · 大 · 更大 · 最大之一",
"text": "用耶稣在约翰福音17章的祷告为他们祷告。祝福:永生,认识神和他所差来的耶稣。大祝福:他们自己的信徒带领别人信主。更大的祝福:属于他们自己的新属灵家庭,在基督里合而为一。最大的祝福之一:从他们中间兴起领袖,装备别人建立新的属灵家庭,一代又一代。求这一切,还有更多,超过我们所求所想的。也像耶稣一样,求神使他们合而为一,保守他们脱离那恶者。若耶稣不是他自己所说的那一位,这一切都不可能。",
"refs": [
"John 17:3,11–23",
"Ephesians 3:20–21"
"John 17:3",
"John 17:11–23",
"Ephesians 3:20"
]
}
],
@ -3584,7 +3575,6 @@
"sets_bounded_title": "已触及与未触及",
"sets_centered_title": "与邻近族群的距离",
"sets_bounded_tally": "{inside} 圈内 · {outside} 圈外 · {unknown} 未知",
"sets_no_data": "{n} 无数据",
"sets_centered_tally": "{placed} 已定位 · {centre} 在中心 · {unplaced} 未定位",
"ring_band_dialect": "同一方言",
"ring_band_language": "同一语言",
@ -3619,8 +3609,7 @@
"Haggai": "哈该书",
"Isaiah": "以赛亚书",
"Habakkuk": "哈巴谷书",
"Ephesians": "以弗所书",
"Revelation": "启示录"
"Ephesians": "以弗所书"
},
"christian_per_cell": "每格约 {n} 人",
"howto_title": "如何使用这份祷告名单",

View file

@ -289,14 +289,10 @@ const HEAD_COLUMNS_CSS = `
// at 22 / 21px with darker ink: about 8pt on the printed page. Only the full layout (HEAD_COLUMNS) gets this; the
// series-wide rules above stay as they were, so the series' SPARSE posters are byte-identical.
// Print size = sheet px x 0.92 (the packet's margins) x 0.408 (1500px = 8.5in): 22px ≈ 8.3pt, 19px ≈ 7.1pt.
// ★ THE NAMES COME FIRST (2026-10-03): the header columns step down to 18px (≈6.8pt; keys 15.5, headings 19) so
// the people-group names beside the map can be the larger type. The shorter band gives the map ~80px more height,
// and the names' cap on this layout rises to 19px (see MAXF). These sizes must equal HC's (fs, keyFs, headFs).
const HEAD_COLUMNS_FULL_CSS = `
.hcolk{font-size:15.5px;fill:#2f2a22;font-weight:800;letter-spacing:.1em}
.hcolt{font-size:18px;fill:#3a362c}
.hcolb{font-weight:700;fill:#2f2a22}
.hcolh{font-family:Georgia,'Times New Roman',serif;font-size:19px;font-weight:700;fill:#2f2a22;letter-spacing:.04em;font-variant:small-caps}
.hcolk{font-size:19px;fill:#2f2a22;font-weight:800;letter-spacing:.1em}
.hcolt{font-size:22px;fill:#3a362c}
.hcolh{font-family:Georgia,'Times New Roman',serif;font-size:23px;font-weight:700;fill:#2f2a22;letter-spacing:.04em;font-variant:small-caps}
.hvref{font-size:19px}
.hverse{font-size:19.5px}
.hsrc{font-size:22px;fill:#4a4636}
@ -696,9 +692,7 @@ function layoutLabels(pins, project) {
col.forEach((it) => { it.maxNum = MAXNUM_L; });
// A map-only sheet frees the header and footer bands the same way SPARSE frees the vision
// columns, so it gets SPARSE's ceiling; the solver still only uses what the column can hold.
// The three-column sheet (HEAD_COLUMNS) gets it too: there the names are the main text, and the cap of 15
// held 21 of the 28 zones' names at 15px (≈5.6pt printed) with room to spare in the band.
const MAXF = (SPARSE || MAP_ONLY || HEAD_COLUMNS) ? 19 : 15;
const MAXF = (SPARSE || MAP_ONLY) ? 19 : 15;
// AVAIL IS THE COLUMN, AND THE NAME DOES NOT GET ALL OF IT.
// The renderer draws the name starting one number-column IN from the column's
// map-facing edge (`nameEdge = tx ± numColW`) but does not shrink its budget by
@ -732,23 +726,18 @@ function layoutLabels(pins, project) {
if (line) out.push(line);
return out.length ? out : [it.name];
};
// The air between rows, as a share of the size. The three-column sheet sets it tighter (0.22): its densest
// zones (95-118 names) are held by the band's height, and the tighter rows turn that height into type
// (west-africa 12.5 -> 13.8px, laos 15.8 -> 17.3px). A row's underline sits 0.30 below its baseline and the
// next row's capitals start 0.56 below it, so they stay clear. Every other sheet keeps 0.34.
const GAPF = HEAD_COLUMNS ? 0.22 : 0.34;
let font = MAXF, wraps = null;
for (let f = MAXF; f >= 8; f -= 0.25) {
const ws = col.map((it) => wrapName(it, f));
const totalLines = ws.reduce((a, w) => a + w.length, 0);
// Each line needs f*1.06 of pitch, plus a little air between rows.
if (totalLines * (f * 1.06) + col.length * (f * GAPF) <= bandH) { font = f; wraps = ws; break; }
if (totalLines * (f * 1.06) + col.length * (f * 0.34) <= bandH) { font = f; wraps = ws; break; }
}
if (!wraps) wraps = col.map((it) => wrapName(it, font));
// Re-pitch rows by LINE COUNT so a two-line name gets the room it needs and
// single-line rows are not stretched to match it.
const lineH = font * 1.06;
const gap = font * GAPF;
const gap = font * 0.34;
const usedH = wraps.reduce((a, w) => a + w.length * lineH, 0) + col.length * gap;
const slack = Math.max(0, bandH - usedH) / Math.max(1, col.length);
let cursor = bandTop + slack / 2;
@ -843,8 +832,7 @@ function paraInto(push, text, { x, y, w, fs, lh, cls, lead }) {
// 1 Jesus' strategy to feed the 5000
// 2 the facts: a bold key above each value (countries, administrative areas, people groups, series)
// 3 what the series is
// The sizes equal HEAD_COLUMNS_FULL_CSS (.hcolt = fs, .hcolk = keyFs, .hcolh = headFs); leading and gaps scale with them.
const HC = { gut: 32, fs: 18, lh: 22, keyFs: 15.5, keyLh: 19.5, headLh: 24.5, gap: 7, headFs: 19 };
const HC = { gut: 32, fs: 22, lh: 27, keyFs: 19, keyLh: 24, headLh: 30, gap: 8, headFs: 23 };
// textW is calibrated for Lato. Without Lato the browser falls back to a system sans that sets
// these columns 2-6% wider (measured 1.05-1.06x in DejaVu Sans). Each block is centred on its
// width at 1.03x, halfway, so any overrun splits across both sides instead of all running right.
@ -865,50 +853,17 @@ const strategyParas = () => (POSTER_TEXT.strategyBody ? [].concat(POSTER_TEXT.st
// series.json (poster.sideBoxMore: one paragraph, or a list; [] prints none), as UPG100 does to keep its band even.
const whatIsMore = () => (POSTER_TEXT.sideBoxMore !== undefined ? [].concat(POSTER_TEXT.sideBoxMore) : [WHAT_IS_MORE]);
const WHAT_IS_MORE = 'Each map comes with a prayer list of its fifty to a hundred people groups — their names, a representative photo, and more detail — to mobilise informed intercession.';
// A paragraph's lines as SVG text content, its **...** runs set bold (.hcolb); the markers are not printed.
// The wrap measures the plain text: a short bold run is a few px wider, inside the measure's room.
function boldLines(text, width) {
let plain = '', bold = [], on = false;
for (const part of String(text).replace(/\s+/g, ' ').trim().split('**')) {
for (let i = 0; i < part.length; i++) { plain += part[i]; bold.push(on); } // per UTF-16 unit, as plain[p] reads
on = !on;
}
// A scripture reference never breaks between its book and its chapter:verse ("Mark 6:40"): that space becomes a
// placeholder for the wrap and prints as a space again. One character for one, so the bold flags stay aligned.
plain = plain.replace(/([A-Za-z]+) (\d+:\d+)/g, '$1\u0001$2');
let p = 0;
return wrapPx(plain, HC.fs, width).map((ln) => {
while (plain[p] === ' ') p++;
let out = '', run = '', runBold = null;
const flush = () => { if (run) out += runBold ? `<tspan class="hcolb">${esc(run)}</tspan>` : esc(run); run = ''; };
for (let k = 0; k < ln.length; k++, p++) {
if (bold[p] !== runBold) { flush(); runBold = bold[p]; }
run += plain[p] === '\u0001' ? ' ' : plain[p];
}
flush();
return out;
});
}
function headColumnsBand(top, facts, approxLists) {
const GRID_L = BAND_MARGIN_G, GRID_W = PW - 2 * BAND_MARGIN_G;
const AVAIL = GRID_W - 2 * HC.gut;
const CW = (GRID_W - 2 * HC.gut) / 3;
// The measure leaves room for a fallback font (see HC_FALLBACK): a block set to it stays inside its column.
const measure = (cw) => (cw - 10 - BAND_TEXT_PAD / 2) * 0.92;
// ★ EVEN COLUMNS (2026-10-03): when the facts column runs longer than the two text columns it is WIDENED (and
// shorter, it is narrowed) so the three end at about the same height. The two side columns stay equal, so they
// still mirror each other round the centre. The centre's share is searched from 26% to 46% of the three-column
// width; the split whose TALLEST column is shortest wins, and a tie goes to the split nearest equal widths, so a
// zone that is already balanced keeps equal columns. measureHeader calls this same function, so the map below
// gets exactly the height that is left.
const TW = (CW - 10 - BAND_TEXT_PAD / 2) * 0.92;
// Uppercase, letterspaced keys: measured as capitals plus the .14em tracking.
const keyW = (k) => textW(k.toUpperCase(), HC.keyFs) + k.length * HC.keyFs * 0.14;
// Each column's height at a given measure (the same arithmetic the drawing below advances y by).
const proseH = (col, tw) => HC.headLh + col[0].paras.reduce((h, t, k) => h + (k ? HC.gap : 0) + boldLines(t, tw).length * HC.lh, 0);
const factsH = (tw) => facts.reduce((h, [, value], k) => h + (k ? HC.gap : 0) + HC.keyLh + wrapPx(value, HC.fs, tw).length * HC.lh, 0);
// Column order (2026-10-02): Jesus' strategy | the facts | what the series is.
const cols = [
[{ head: 'Jesus’ Strategy to Feed the 5000', paras: strategyParas() }],
null, // the facts column, wrapped below at the centre's measure
facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, TW) })),
[{ head: POSTER_TEXT.sideBoxHeading || 'What is a UPG100 List?', paras: [
(POSTER_TEXT.sideBoxBody
|| 'This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.')
@ -916,22 +871,9 @@ function headColumnsBand(top, facts, approxLists) {
...whatIsMore(),
] }],
];
// The split: equal thirds first, then centre shares on a 1% step ordered by their distance from a third, so
// among splits whose tallest column is equally short, the one nearest equal widths is found first and kept.
const shares = Array.from({ length: 21 }, (_, k) => 0.26 + k * 0.01).sort((a, b) => Math.abs(a - 1 / 3) - Math.abs(b - 1 / 3));
let best = null;
for (const cwC of [1 / 3, ...shares].map((f) => AVAIL * f)) {
const cwS = (AVAIL - cwC) / 2;
const tall = Math.max(proseH(cols[0], measure(cwS)), factsH(measure(cwC)), proseH(cols[2], measure(cwS)));
if (!best || tall < best.tall - 0.5) best = { cwC, cwS, tall };
}
const widths = [best.cwS, best.cwC, best.cwS];
const lefts = [GRID_L, GRID_L + best.cwS + HC.gut, GRID_L + best.cwS + HC.gut + best.cwC + HC.gut];
cols[1] = facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, measure(best.cwC)) }));
let frags = '', bottom = top;
cols.forEach((col, i) => {
const CW = widths[i], TW = measure(CW);
const centre = lefts[i] + CW / 2;
const centre = GRID_L + i * (CW + HC.gut) + CW / 2;
if (col[0] && col[0].key !== undefined) { // the facts column: a bold key above each value
const blockW = Math.min(TW, Math.max(...col.map((r) => Math.max(keyW(r.key), ...r.lines.map((ln) => textW(ln, HC.fs))))));
const x = (centre - blockW * HC_FALLBACK / 2).toFixed(1);
@ -945,22 +887,20 @@ function headColumnsBand(top, facts, approxLists) {
bottom = Math.max(bottom, y);
} else {
const { head, paras } = col[0];
// ★ MIRRORED (2026-10-03): both prose columns take the full measure, so each sits the same distance from
// its rule and the two outer columns mirror each other round the centred facts column. (Each used to be
// centred on its own widest line, so the two could sit at different distances from their rules.)
const blockW = TW;
const wrapped = paras.map((t) => wrapPx(t, HC.fs, TW));
const blockW = Math.min(TW, Math.max(textW(head, HC.headFs), ...wrapped.flat().map((ln) => textW(ln, HC.fs))));
const x = (centre - blockW * HC_FALLBACK / 2).toFixed(1);
frags += `<text x="${x}" y="${top}" class="hcolh">${esc(head)}</text>`;
let y = top + HC.headLh;
paras.forEach((text, k) => {
wrapped.forEach((lines, k) => {
if (k) y += HC.gap;
for (const ln of boldLines(text, TW)) { frags += `<text x="${x}" y="${y}" class="hcolt">${ln}</text>`; y += HC.lh; }
for (const ln of lines) { frags += `<text x="${x}" y="${y}" class="hcolt">${esc(ln)}</text>`; y += HC.lh; }
});
bottom = Math.max(bottom, y);
}
});
for (let i = 1; i < 3; i++) {
const rx = (lefts[i] - HC.gut / 2).toFixed(1);
const rx = (GRID_L + i * (CW + HC.gut) - HC.gut / 2).toFixed(1);
frags += `<line x1="${rx}" y1="${top - 14}" x2="${rx}" y2="${bottom - 12}" class="hcolrule"/>`;
}
return { frags, bottom };
@ -2406,7 +2346,7 @@ async function renderPoster(zone, pins, zoneFC) {
let cy0 = bodyTop;
strategyParas().forEach((text, k) => {
if (k) cy0 += 5;
cy0 = paraInto(push, String(text).replace(/\*\*/g, ''), { x: b1x[0], y: cy0, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
cy0 = paraInto(push, text, { x: b1x[0], y: cy0, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
});
let cy1 = bodyTop;
@ -2417,11 +2357,9 @@ async function renderPoster(zone, pins, zoneFC) {
|| 'This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.')
.replace('{approxLists}', String(approxLists))
.replace('{seriesName}', SERIES_NAME);
// A **...** bold marker is the band's (boldLines); this classic header prints plain, so the markers are dropped.
const plain = (t) => String(t).replace(/\*\*/g, '');
[whatIs, ...whatIsMore()].forEach((text, k) => {
if (k) cy1 += 5;
cy1 = paraInto(push, plain(text), { x: b1x[1], y: cy1, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
cy1 = paraInto(push, text, { x: b1x[1], y: cy1, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
});
const b1Bot = Math.max(cy0, cy1);

View file

@ -287,14 +287,10 @@ const HEAD_COLUMNS_CSS = `
// at 22 / 21px with darker ink: about 8pt on the printed page. Only the full layout (HEAD_COLUMNS) gets this; the
// series-wide rules above stay as they were, so the series' SPARSE posters are byte-identical.
// Print size = sheet px x 0.92 (the packet's margins) x 0.408 (1500px = 8.5in): 22px ≈ 8.3pt, 19px ≈ 7.1pt.
// ★ THE NAMES COME FIRST (2026-10-03): the header columns step down to 18px (≈6.8pt; keys 15.5, headings 19) so
// the people-group names beside the map can be the larger type. The shorter band gives the map ~80px more height,
// and the names' cap on this layout rises to 19px (see MAXF). These sizes must equal HC's (fs, keyFs, headFs).
const HEAD_COLUMNS_FULL_CSS = `
.hcolk{font-size:15.5px;fill:#2f2a22;font-weight:800;letter-spacing:.1em}
.hcolt{font-size:18px;fill:#3a362c}
.hcolb{font-weight:700;fill:#2f2a22}
.hcolh{font-family:Georgia,'Times New Roman',serif;font-size:19px;font-weight:700;fill:#2f2a22;letter-spacing:.04em;font-variant:small-caps}
.hcolk{font-size:19px;fill:#2f2a22;font-weight:800;letter-spacing:.1em}
.hcolt{font-size:22px;fill:#3a362c}
.hcolh{font-family:Georgia,'Times New Roman',serif;font-size:23px;font-weight:700;fill:#2f2a22;letter-spacing:.04em;font-variant:small-caps}
.hvref{font-size:19px}
.hverse{font-size:19.5px}
.hsrc{font-size:22px;fill:#4a4636}
@ -694,9 +690,7 @@ function layoutLabels(pins, project) {
col.forEach((it) => { it.maxNum = MAXNUM_L; });
// A map-only sheet frees the header and footer bands the same way SPARSE frees the vision
// columns, so it gets SPARSE's ceiling; the solver still only uses what the column can hold.
// The three-column sheet (HEAD_COLUMNS) gets it too: there the names are the main text, and the cap of 15
// held 21 of the 28 zones' names at 15px (≈5.6pt printed) with room to spare in the band.
const MAXF = (SPARSE || MAP_ONLY || HEAD_COLUMNS) ? 19 : 15;
const MAXF = (SPARSE || MAP_ONLY) ? 19 : 15;
// AVAIL IS THE COLUMN, AND THE NAME DOES NOT GET ALL OF IT.
// The renderer draws the name starting one number-column IN from the column's
// map-facing edge (`nameEdge = tx ± numColW`) but does not shrink its budget by
@ -730,23 +724,18 @@ function layoutLabels(pins, project) {
if (line) out.push(line);
return out.length ? out : [it.name];
};
// The air between rows, as a share of the size. The three-column sheet sets it tighter (0.22): its densest
// zones (95-118 names) are held by the band's height, and the tighter rows turn that height into type
// (west-africa 12.5 -> 13.8px, laos 15.8 -> 17.3px). A row's underline sits 0.30 below its baseline and the
// next row's capitals start 0.56 below it, so they stay clear. Every other sheet keeps 0.34.
const GAPF = HEAD_COLUMNS ? 0.22 : 0.34;
let font = MAXF, wraps = null;
for (let f = MAXF; f >= 8; f -= 0.25) {
const ws = col.map((it) => wrapName(it, f));
const totalLines = ws.reduce((a, w) => a + w.length, 0);
// Each line needs f*1.06 of pitch, plus a little air between rows.
if (totalLines * (f * 1.06) + col.length * (f * GAPF) <= bandH) { font = f; wraps = ws; break; }
if (totalLines * (f * 1.06) + col.length * (f * 0.34) <= bandH) { font = f; wraps = ws; break; }
}
if (!wraps) wraps = col.map((it) => wrapName(it, font));
// Re-pitch rows by LINE COUNT so a two-line name gets the room it needs and
// single-line rows are not stretched to match it.
const lineH = font * 1.06;
const gap = font * GAPF;
const gap = font * 0.34;
const usedH = wraps.reduce((a, w) => a + w.length * lineH, 0) + col.length * gap;
const slack = Math.max(0, bandH - usedH) / Math.max(1, col.length);
let cursor = bandTop + slack / 2;
@ -841,8 +830,7 @@ function paraInto(push, text, { x, y, w, fs, lh, cls, lead }) {
// 1 Jesus' strategy to feed the 5000
// 2 the facts: a bold key above each value (countries, administrative areas, people groups, series)
// 3 what the series is
// The sizes equal HEAD_COLUMNS_FULL_CSS (.hcolt = fs, .hcolk = keyFs, .hcolh = headFs); leading and gaps scale with them.
const HC = { gut: 32, fs: 18, lh: 22, keyFs: 15.5, keyLh: 19.5, headLh: 24.5, gap: 7, headFs: 19 };
const HC = { gut: 32, fs: 22, lh: 27, keyFs: 19, keyLh: 24, headLh: 30, gap: 8, headFs: 23 };
// textW is calibrated for Lato. Without Lato the browser falls back to a system sans that sets
// these columns 2-6% wider (measured 1.05-1.06x in DejaVu Sans). Each block is centred on its
// width at 1.03x, halfway, so any overrun splits across both sides instead of all running right.
@ -863,50 +851,17 @@ const strategyParas = () => (POSTER_TEXT.strategyBody ? [].concat(POSTER_TEXT.st
// series.json (poster.sideBoxMore: one paragraph, or a list; [] prints none), as UPG100 does to keep its band even.
const whatIsMore = () => (POSTER_TEXT.sideBoxMore !== undefined ? [].concat(POSTER_TEXT.sideBoxMore) : [WHAT_IS_MORE]);
const WHAT_IS_MORE = 'Each map comes with a prayer list of its fifty to a hundred people groups — their names, a representative photo, and more detail — to mobilise informed intercession.';
// A paragraph's lines as SVG text content, its **...** runs set bold (.hcolb); the markers are not printed.
// The wrap measures the plain text: a short bold run is a few px wider, inside the measure's room.
function boldLines(text, width) {
let plain = '', bold = [], on = false;
for (const part of String(text).replace(/\s+/g, ' ').trim().split('**')) {
for (let i = 0; i < part.length; i++) { plain += part[i]; bold.push(on); } // per UTF-16 unit, as plain[p] reads
on = !on;
}
// A scripture reference never breaks between its book and its chapter:verse ("Mark 6:40"): that space becomes a
// placeholder for the wrap and prints as a space again. One character for one, so the bold flags stay aligned.
plain = plain.replace(/([A-Za-z]+) (\d+:\d+)/g, '$1\u0001$2');
let p = 0;
return wrapPx(plain, HC.fs, width).map((ln) => {
while (plain[p] === ' ') p++;
let out = '', run = '', runBold = null;
const flush = () => { if (run) out += runBold ? `<tspan class="hcolb">${esc(run)}</tspan>` : esc(run); run = ''; };
for (let k = 0; k < ln.length; k++, p++) {
if (bold[p] !== runBold) { flush(); runBold = bold[p]; }
run += plain[p] === '\u0001' ? ' ' : plain[p];
}
flush();
return out;
});
}
function headColumnsBand(top, facts, approxLists) {
const GRID_L = BAND_MARGIN_G, GRID_W = PW - 2 * BAND_MARGIN_G;
const AVAIL = GRID_W - 2 * HC.gut;
const CW = (GRID_W - 2 * HC.gut) / 3;
// The measure leaves room for a fallback font (see HC_FALLBACK): a block set to it stays inside its column.
const measure = (cw) => (cw - 10 - BAND_TEXT_PAD / 2) * 0.92;
// ★ EVEN COLUMNS (2026-10-03): when the facts column runs longer than the two text columns it is WIDENED (and
// shorter, it is narrowed) so the three end at about the same height. The two side columns stay equal, so they
// still mirror each other round the centre. The centre's share is searched from 26% to 46% of the three-column
// width; the split whose TALLEST column is shortest wins, and a tie goes to the split nearest equal widths, so a
// zone that is already balanced keeps equal columns. measureHeader calls this same function, so the map below
// gets exactly the height that is left.
const TW = (CW - 10 - BAND_TEXT_PAD / 2) * 0.92;
// Uppercase, letterspaced keys: measured as capitals plus the .14em tracking.
const keyW = (k) => textW(k.toUpperCase(), HC.keyFs) + k.length * HC.keyFs * 0.14;
// Each column's height at a given measure (the same arithmetic the drawing below advances y by).
const proseH = (col, tw) => HC.headLh + col[0].paras.reduce((h, t, k) => h + (k ? HC.gap : 0) + boldLines(t, tw).length * HC.lh, 0);
const factsH = (tw) => facts.reduce((h, [, value], k) => h + (k ? HC.gap : 0) + HC.keyLh + wrapPx(value, HC.fs, tw).length * HC.lh, 0);
// Column order (2026-10-02): Jesus' strategy | the facts | what the series is.
const cols = [
[{ head: 'Jesus’ Strategy to Feed the 5000', paras: strategyParas() }],
null, // the facts column, wrapped below at the centre's measure
facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, TW) })),
[{ head: POSTER_TEXT.sideBoxHeading || 'What is a UPG100 List?', paras: [
(POSTER_TEXT.sideBoxBody
|| 'This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.')
@ -914,22 +869,9 @@ function headColumnsBand(top, facts, approxLists) {
...whatIsMore(),
] }],
];
// The split: equal thirds first, then centre shares on a 1% step ordered by their distance from a third, so
// among splits whose tallest column is equally short, the one nearest equal widths is found first and kept.
const shares = Array.from({ length: 21 }, (_, k) => 0.26 + k * 0.01).sort((a, b) => Math.abs(a - 1 / 3) - Math.abs(b - 1 / 3));
let best = null;
for (const cwC of [1 / 3, ...shares].map((f) => AVAIL * f)) {
const cwS = (AVAIL - cwC) / 2;
const tall = Math.max(proseH(cols[0], measure(cwS)), factsH(measure(cwC)), proseH(cols[2], measure(cwS)));
if (!best || tall < best.tall - 0.5) best = { cwC, cwS, tall };
}
const widths = [best.cwS, best.cwC, best.cwS];
const lefts = [GRID_L, GRID_L + best.cwS + HC.gut, GRID_L + best.cwS + HC.gut + best.cwC + HC.gut];
cols[1] = facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, measure(best.cwC)) }));
let frags = '', bottom = top;
cols.forEach((col, i) => {
const CW = widths[i], TW = measure(CW);
const centre = lefts[i] + CW / 2;
const centre = GRID_L + i * (CW + HC.gut) + CW / 2;
if (col[0] && col[0].key !== undefined) { // the facts column: a bold key above each value
const blockW = Math.min(TW, Math.max(...col.map((r) => Math.max(keyW(r.key), ...r.lines.map((ln) => textW(ln, HC.fs))))));
const x = (centre - blockW * HC_FALLBACK / 2).toFixed(1);
@ -943,22 +885,20 @@ function headColumnsBand(top, facts, approxLists) {
bottom = Math.max(bottom, y);
} else {
const { head, paras } = col[0];
// ★ MIRRORED (2026-10-03): both prose columns take the full measure, so each sits the same distance from
// its rule and the two outer columns mirror each other round the centred facts column. (Each used to be
// centred on its own widest line, so the two could sit at different distances from their rules.)
const blockW = TW;
const wrapped = paras.map((t) => wrapPx(t, HC.fs, TW));
const blockW = Math.min(TW, Math.max(textW(head, HC.headFs), ...wrapped.flat().map((ln) => textW(ln, HC.fs))));
const x = (centre - blockW * HC_FALLBACK / 2).toFixed(1);
frags += `<text x="${x}" y="${top}" class="hcolh">${esc(head)}</text>`;
let y = top + HC.headLh;
paras.forEach((text, k) => {
wrapped.forEach((lines, k) => {
if (k) y += HC.gap;
for (const ln of boldLines(text, TW)) { frags += `<text x="${x}" y="${y}" class="hcolt">${ln}</text>`; y += HC.lh; }
for (const ln of lines) { frags += `<text x="${x}" y="${y}" class="hcolt">${esc(ln)}</text>`; y += HC.lh; }
});
bottom = Math.max(bottom, y);
}
});
for (let i = 1; i < 3; i++) {
const rx = (lefts[i] - HC.gut / 2).toFixed(1);
const rx = (GRID_L + i * (CW + HC.gut) - HC.gut / 2).toFixed(1);
frags += `<line x1="${rx}" y1="${top - 14}" x2="${rx}" y2="${bottom - 12}" class="hcolrule"/>`;
}
return { frags, bottom };
@ -2404,7 +2344,7 @@ async function renderPoster(zone, pins, zoneFC) {
let cy0 = bodyTop;
strategyParas().forEach((text, k) => {
if (k) cy0 += 5;
cy0 = paraInto(push, String(text).replace(/\*\*/g, ''), { x: b1x[0], y: cy0, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
cy0 = paraInto(push, text, { x: b1x[0], y: cy0, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
});
let cy1 = bodyTop;
@ -2415,11 +2355,9 @@ async function renderPoster(zone, pins, zoneFC) {
|| 'This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.')
.replace('{approxLists}', String(approxLists))
.replace('{seriesName}', SERIES_NAME);
// A **...** bold marker is the band's (boldLines); this classic header prints plain, so the markers are dropped.
const plain = (t) => String(t).replace(/\*\*/g, '');
[whatIs, ...whatIsMore()].forEach((text, k) => {
if (k) cy1 += 5;
cy1 = paraInto(push, plain(text), { x: b1x[1], y: cy1, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
cy1 = paraInto(push, text, { x: b1x[1], y: cy1, w: B1_TW, fs: B1_FS, lh: B1_LH, cls: 'hsrc2' });
});
const b1Bot = Math.max(cy0, cy1);

File diff suppressed because one or more lines are too long

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bless-india-posters> custom element. -->
<script src="../bless-india-posters.js" integrity="sha384-Y/Ib6sYGClrODPxG3Gj9txadwtVZRIiHWQa9FA9fH/RGAAs9SAbVrJobrwC4bY1b" crossorigin="anonymous"></script>
<script src="../bless-india-posters.js" integrity="sha384-E4XxvUZ7ZN9VXwwUAYAMldJNsIRq1ijNibobg0flroCtP9b33aMpwxYcMpA65blB" 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

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-6c4SlWMUWowE4IG1S5F3WshA66I3aKnK7ilHyHzOncLWeyXFL/I+NGlAYhyjZ1Ql" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-4lP0sd8cKRr7zxTWiTkkIq2JS4gDshLK8Cd5kj+RyawyO47mtQXandtVJWqttu2R" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-6c4SlWMUWowE4IG1S5F3WshA66I3aKnK7ilHyHzOncLWeyXFL/I+NGlAYhyjZ1Ql" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-4lP0sd8cKRr7zxTWiTkkIq2JS4gDshLK8Cd5kj+RyawyO47mtQXandtVJWqttu2R" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-6c4SlWMUWowE4IG1S5F3WshA66I3aKnK7ilHyHzOncLWeyXFL/I+NGlAYhyjZ1Ql" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-4lP0sd8cKRr7zxTWiTkkIq2JS4gDshLK8Cd5kj+RyawyO47mtQXandtVJWqttu2R" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -82,7 +82,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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco" 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

@ -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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco" 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

@ -82,7 +82,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../../dashboard-plus-grid.js" integrity="sha384-AyqUww8eDFdF36GeDkZhv3wX6orazKJ4ksNFWdTclwRXYYYTyJ3zgHdoU204eiYk" crossorigin="anonymous"></script>
<script src="../../dashboard-plus-grid.js" integrity="sha384-6eSrxg390nmpP3NsF1ViQX0ez3SuSwDQc6lCVH3M064cUpYe1SmEIzCT9obKVW6O" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -82,7 +82,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../dashboard-plus-grid.js" integrity="sha384-AyqUww8eDFdF36GeDkZhv3wX6orazKJ4ksNFWdTclwRXYYYTyJ3zgHdoU204eiYk" crossorigin="anonymous"></script>
<script src="../dashboard-plus-grid.js" integrity="sha384-6eSrxg390nmpP3NsF1ViQX0ez3SuSwDQc6lCVH3M064cUpYe1SmEIzCT9obKVW6O" 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

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
<script src="../demo-map.js" integrity="sha384-eCdagq/JzSB4RbPXxCB4Cah0DemoKxPhkguZy1jvFoAN5ge0XzUDxid7ln8rHqAX" crossorigin="anonymous"></script>
<script src="../demo-map.js" integrity="sha384-MRBwmDqq8PPB9Lo5nRbJTG4fvc9IzZIm7XXDIjszYtcy8JehJ7i8tuDhmo27ErpZ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -72,7 +72,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
<script src="../../demo-map.js" integrity="sha384-eCdagq/JzSB4RbPXxCB4Cah0DemoKxPhkguZy1jvFoAN5ge0XzUDxid7ln8rHqAX" crossorigin="anonymous"></script>
<script src="../../demo-map.js" integrity="sha384-MRBwmDqq8PPB9Lo5nRbJTG4fvc9IzZIm7XXDIjszYtcy8JehJ7i8tuDhmo27ErpZ" 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

@ -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-Wop35cwyXSmt+i3+2mobkc03oOirRiyyL4nx22fiPqN5uo0uQpegH4hE9PQnpHx7" crossorigin="anonymous"></script>
<script src="../../doxa-maps-spa.js" integrity="sha384-JbdbnT273exxkEbpgtOE3DxfYeQ1iPRockZeONzgkP1TJ3dITNCl4yRJ5KMXiXQb" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-Wop35cwyXSmt+i3+2mobkc03oOirRiyyL4nx22fiPqN5uo0uQpegH4hE9PQnpHx7" crossorigin="anonymous"></script>
<script src="../doxa-maps-spa.js" integrity="sha384-JbdbnT273exxkEbpgtOE3DxfYeQ1iPRockZeONzgkP1TJ3dITNCl4yRJ5KMXiXQb" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -12,7 +12,7 @@
"bundle": "all-people-groups",
"scriptUrl": "./all-people-groups/all-people-groups.js",
"output": "dx-maps/all-people-groups/all-people-groups.js",
"integrity": "sha384-4uZxLJxLey7Zl7tiZ5qTwrQnblpRX0lxzKzjTcfUqBNEOP2Qn63Bvtra+Vo+6ecm",
"integrity": "sha384-XZupcGRo5bafMWWJsYcOJVOhT4Mw4Vv/v4hgVKMrQU5X8IoRLP11Z9PoGkbxbxLS",
"elements": [
"all-people-groups"
],
@ -30,8 +30,8 @@
"scriptUrl": "./all-people-groups/all-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./all-people-groups/all-people-groups/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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-4uZxLJxLey7Zl7tiZ5qTwrQnblpRX0lxzKzjTcfUqBNEOP2Qn63Bvtra+Vo+6ecm\" 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<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>",
"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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-4uZxLJxLey7Zl7tiZ5qTwrQnblpRX0lxzKzjTcfUqBNEOP2Qn63Bvtra+Vo+6ecm\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
"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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-XZupcGRo5bafMWWJsYcOJVOhT4Mw4Vv/v4hgVKMrQU5X8IoRLP11Z9PoGkbxbxLS\" 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<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>",
"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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-XZupcGRo5bafMWWJsYcOJVOhT4Mw4Vv/v4hgVKMrQU5X8IoRLP11Z9PoGkbxbxLS\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
},
"bundle": "all-people-groups"
}
@ -164,7 +164,7 @@
"bundle": "bless-india-posters",
"scriptUrl": "./bless-india-posters/bless-india-posters.js",
"output": "dx-maps/bless-india-posters/bless-india-posters.js",
"integrity": "sha384-Y/Ib6sYGClrODPxG3Gj9txadwtVZRIiHWQa9FA9fH/RGAAs9SAbVrJobrwC4bY1b",
"integrity": "sha384-E4XxvUZ7ZN9VXwwUAYAMldJNsIRq1ijNibobg0flroCtP9b33aMpwxYcMpA65blB",
"elements": [
"bless-india-posters"
],
@ -207,7 +207,7 @@
"scriptUrl": "./bless-india-posters/bless-india-posters.js",
"embed": {
"iframe": "<iframe src=\"./bless-india-posters/bless-india-posters/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=\"./bless-india-posters/bless-india-posters.js\" integrity=\"sha384-Y/Ib6sYGClrODPxG3Gj9txadwtVZRIiHWQa9FA9fH/RGAAs9SAbVrJobrwC4bY1b\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<bless-india-posters profile-config='{\"profile\":\"bless-india-posters\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bless-india-posters>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./bless-india-posters/bless-india-posters.js\" integrity=\"sha384-E4XxvUZ7ZN9VXwwUAYAMldJNsIRq1ijNibobg0flroCtP9b33aMpwxYcMpA65blB\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<bless-india-posters profile-config='{\"profile\":\"bless-india-posters\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bless-india-posters>"
}
}
]
@ -216,7 +216,7 @@
"bundle": "cities-internet",
"scriptUrl": "./cities-internet/cities-internet.js",
"output": "dx-maps/cities-internet/cities-internet.js",
"integrity": "sha384-6c4SlWMUWowE4IG1S5F3WshA66I3aKnK7ilHyHzOncLWeyXFL/I+NGlAYhyjZ1Ql",
"integrity": "sha384-4lP0sd8cKRr7zxTWiTkkIq2JS4gDshLK8Cd5kj+RyawyO47mtQXandtVJWqttu2R",
"elements": [
"cities-internet-map"
],
@ -235,7 +235,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-6c4SlWMUWowE4IG1S5F3WshA66I3aKnK7ilHyHzOncLWeyXFL/I+NGlAYhyjZ1Ql\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-4lP0sd8cKRr7zxTWiTkkIq2JS4gDshLK8Cd5kj+RyawyO47mtQXandtVJWqttu2R\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -251,8 +251,8 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- 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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-6c4SlWMUWowE4IG1S5F3WshA66I3aKnK7ilHyHzOncLWeyXFL/I+NGlAYhyjZ1Ql\" 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<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-6c4SlWMUWowE4IG1S5F3WshA66I3aKnK7ilHyHzOncLWeyXFL/I+NGlAYhyjZ1Ql\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-4lP0sd8cKRr7zxTWiTkkIq2JS4gDshLK8Cd5kj+RyawyO47mtQXandtVJWqttu2R\" 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<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-4lP0sd8cKRr7zxTWiTkkIq2JS4gDshLK8Cd5kj+RyawyO47mtQXandtVJWqttu2R\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -268,7 +268,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-6c4SlWMUWowE4IG1S5F3WshA66I3aKnK7ilHyHzOncLWeyXFL/I+NGlAYhyjZ1Ql\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-4lP0sd8cKRr7zxTWiTkkIq2JS4gDshLK8Cd5kj+RyawyO47mtQXandtVJWqttu2R\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
}
]
@ -306,7 +306,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z",
"integrity": "sha384-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco",
"elements": [
"country-posters-map"
],
@ -329,8 +329,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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z\" 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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z\" 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-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco\" 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-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco\" 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>"
}
},
{
@ -368,7 +368,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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z\" 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-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco\" 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>"
}
},
{
@ -384,8 +384,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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z\" 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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z\" 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-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco\" 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-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco\" 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>"
}
},
{
@ -401,7 +401,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/poster-section/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-PWyg9lVVg1fuZP5YCEBCsoAonqUcYztXk0ykfXnlPDA1eqeLNZHYKokaon1mE90Z\" 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\":\"poster-section\",\"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-DYMook96C1FS2TXi4vfEEWwRJogIkt45FZU692hxDFpOdv3W36M3MtmWgZguihco\" 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\":\"poster-section\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -410,7 +410,7 @@
"bundle": "dashboard-plus-grid",
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
"integrity": "sha384-AyqUww8eDFdF36GeDkZhv3wX6orazKJ4ksNFWdTclwRXYYYTyJ3zgHdoU204eiYk",
"integrity": "sha384-6eSrxg390nmpP3NsF1ViQX0ez3SuSwDQc6lCVH3M064cUpYe1SmEIzCT9obKVW6O",
"elements": [
"dashboard-plus-grid"
],
@ -434,8 +434,8 @@
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"embed": {
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-AyqUww8eDFdF36GeDkZhv3wX6orazKJ4ksNFWdTclwRXYYYTyJ3zgHdoU204eiYk\" 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<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>",
"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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-AyqUww8eDFdF36GeDkZhv3wX6orazKJ4ksNFWdTclwRXYYYTyJ3zgHdoU204eiYk\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
"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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-6eSrxg390nmpP3NsF1ViQX0ez3SuSwDQc6lCVH3M064cUpYe1SmEIzCT9obKVW6O\" 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<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>",
"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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-6eSrxg390nmpP3NsF1ViQX0ez3SuSwDQc6lCVH3M064cUpYe1SmEIzCT9obKVW6O\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
},
"instances": [
{
@ -459,7 +459,7 @@
"bundle": "demo-map",
"scriptUrl": "./demo-map/demo-map.js",
"output": "dx-maps/demo-map/demo-map.js",
"integrity": "sha384-eCdagq/JzSB4RbPXxCB4Cah0DemoKxPhkguZy1jvFoAN5ge0XzUDxid7ln8rHqAX",
"integrity": "sha384-MRBwmDqq8PPB9Lo5nRbJTG4fvc9IzZIm7XXDIjszYtcy8JehJ7i8tuDhmo27ErpZ",
"elements": [
"demo-map"
],
@ -477,8 +477,8 @@
"scriptUrl": "./demo-map/demo-map.js",
"embed": {
"iframe": "<iframe src=\"./demo-map/demo-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=\"./demo-map/demo-map.js\" integrity=\"sha384-eCdagq/JzSB4RbPXxCB4Cah0DemoKxPhkguZy1jvFoAN5ge0XzUDxid7ln8rHqAX\" 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<demo-map profile-config='{\"profile\":\"demo-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-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=\"./demo-map/demo-map.js\" integrity=\"sha384-eCdagq/JzSB4RbPXxCB4Cah0DemoKxPhkguZy1jvFoAN5ge0XzUDxid7ln8rHqAX\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<demo-map profile-config='{\"profile\":\"demo-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-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=\"./demo-map/demo-map.js\" integrity=\"sha384-MRBwmDqq8PPB9Lo5nRbJTG4fvc9IzZIm7XXDIjszYtcy8JehJ7i8tuDhmo27ErpZ\" 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<demo-map profile-config='{\"profile\":\"demo-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-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=\"./demo-map/demo-map.js\" integrity=\"sha384-MRBwmDqq8PPB9Lo5nRbJTG4fvc9IzZIm7XXDIjszYtcy8JehJ7i8tuDhmo27ErpZ\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<demo-map profile-config='{\"profile\":\"demo-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
},
"instances": [
{
@ -505,7 +505,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-Wop35cwyXSmt+i3+2mobkc03oOirRiyyL4nx22fiPqN5uo0uQpegH4hE9PQnpHx7",
"integrity": "sha384-JbdbnT273exxkEbpgtOE3DxfYeQ1iPRockZeONzgkP1TJ3dITNCl4yRJ5KMXiXQb",
"elements": [
"doxa-maps-spa"
],
@ -535,7 +535,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-Wop35cwyXSmt+i3+2mobkc03oOirRiyyL4nx22fiPqN5uo0uQpegH4hE9PQnpHx7\" 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-JbdbnT273exxkEbpgtOE3DxfYeQ1iPRockZeONzgkP1TJ3dITNCl4yRJ5KMXiXQb\" 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": [
{
@ -562,7 +562,7 @@
"bundle": "doxa-research-map",
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
"integrity": "sha384-w8mEGeIr5DTaQEIrfMaga7c8AOzBfk5BCFQWhhBLStfA1x+8/WPn0W8FVDM77tX+",
"integrity": "sha384-B7rrEPAoDC9HdBjrUUdCZumSu+y/t/j0H8PJ7Q9s7nTBcYY9TeJfSjr6dRAK9a/V",
"elements": [
"doxa-research-map"
],
@ -581,8 +581,8 @@
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-research-map/research-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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-w8mEGeIr5DTaQEIrfMaga7c8AOzBfk5BCFQWhhBLStfA1x+8/WPn0W8FVDM77tX+\" 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<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-w8mEGeIr5DTaQEIrfMaga7c8AOzBfk5BCFQWhhBLStfA1x+8/WPn0W8FVDM77tX+\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-B7rrEPAoDC9HdBjrUUdCZumSu+y/t/j0H8PJ7Q9s7nTBcYY9TeJfSjr6dRAK9a/V\" 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<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-B7rrEPAoDC9HdBjrUUdCZumSu+y/t/j0H8PJ7Q9s7nTBcYY9TeJfSjr6dRAK9a/V\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
},
"instances": [
{
@ -608,7 +608,7 @@
"bundle": "doxa-simple-map",
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"output": "dx-maps/doxa-simple-map/doxa-simple-map.js",
"integrity": "sha384-dVdk9Dyd0DZ92VoOULWQFVRqexr7zKwDyvy9GkGXHe0oGo3tqS+xR1pc04QP/B0L",
"integrity": "sha384-m1zUFDjAPVm/dldyOUCh8fR061OIAUBl5Iclo0iL3wy6P5+wqCeEioL0Dmck3Opn",
"elements": [
"dxm-simple-map"
],
@ -627,8 +627,8 @@
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- 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=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-dVdk9Dyd0DZ92VoOULWQFVRqexr7zKwDyvy9GkGXHe0oGo3tqS+xR1pc04QP/B0L\" 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<dxm-simple-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dxm-simple-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=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-dVdk9Dyd0DZ92VoOULWQFVRqexr7zKwDyvy9GkGXHe0oGo3tqS+xR1pc04QP/B0L\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<dxm-simple-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dxm-simple-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=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-m1zUFDjAPVm/dldyOUCh8fR061OIAUBl5Iclo0iL3wy6P5+wqCeEioL0Dmck3Opn\" 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<dxm-simple-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dxm-simple-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=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-m1zUFDjAPVm/dldyOUCh8fR061OIAUBl5Iclo0iL3wy6P5+wqCeEioL0Dmck3Opn\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<dxm-simple-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dxm-simple-map>"
},
"instances": [
{
@ -798,7 +798,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc",
"integrity": "sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL",
"elements": [
"gospel-internet-board"
],
@ -819,8 +819,8 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "<iframe src=\"./gospel-internet/board/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=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc\" 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<gospel-internet-board profile-config='{\"profile\":\"board\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>",
"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=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
"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=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL\" 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<gospel-internet-board profile-config='{\"profile\":\"board\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>",
"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=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
},
"instances": [
{
@ -927,7 +927,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "dx-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M",
"integrity": "sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q",
"elements": [
"gospel-resources-map"
],
@ -950,7 +950,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -966,7 +966,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -982,8 +982,8 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- 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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M\" 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<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q\" 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<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -999,7 +999,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -1015,8 +1015,8 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- 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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M\" 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<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q\" 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<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
}
]
@ -1025,7 +1025,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-lGHqz/3puE+d+OsmZgC+TmlNYspuG9ifVaIyxyzGyAFf6cXh9wDwSKj9bFvZY4Fo",
"integrity": "sha384-RoUFVUuNTZd9S6IGDGEbW7D975a4q6EFiZH4xdye1ggd3bbArVu2MT3tBYjXVWiD",
"elements": [
"my-upg-100-list-map"
],
@ -1064,43 +1064,43 @@
],
"posterDesignLabels": {
"upg100-territory-poster.letter": {
"ar": "ملصق منطقة UPG100",
"de": "UPG100-Gebietsplakat",
"en": "UPG100 zone poster",
"es": "Cartel de zona UPG100",
"fr": "Affiche de zone UPG100",
"hi": "UPG100 क्षेत्र का पोस्टर",
"it": "Poster di zona UPG100",
"pt": "Cartaz de zona UPG100",
"ro": "Poster de zonă UPG100",
"ru": "Плакат зоны UPG100",
"zh": "UPG100 区域海报(单张)"
"ar": "ملصق منطقة UPG-100",
"de": "UPG-100-Gebietsplakat",
"en": "UPG-100 territory poster",
"es": "Cartel de territorio UPG-100",
"fr": "Affiche de territoire UPG-100",
"hi": "UPG-100 क्षेत्र पोस्टर",
"it": "Manifesto di territorio UPG-100",
"pt": "Cartaz de território UPG-100",
"ro": "Afiș de teritoriu UPG-100",
"ru": "Плакат территории UPG-100",
"zh": "UPG-100 区域海报"
},
"upg100-territory-poster.3x3": {
"ar": "ملصق منطقة UPG100",
"de": "UPG100-Gebietsplakat",
"en": "UPG100 zone poster",
"es": "Cartel de zona UPG100",
"fr": "Affiche de zone UPG100",
"hi": "UPG100 क्षेत्र का पोस्टर",
"it": "Poster di zona UPG100",
"pt": "Cartaz de zona UPG100",
"ro": "Poster de zonă UPG100",
"ru": "Плакат зоны UPG100",
"zh": "UPG100 区域海报(单张)"
"ar": "ملصق منطقة UPG-100",
"de": "UPG-100-Gebietsplakat",
"en": "UPG-100 territory poster",
"es": "Cartel de territorio UPG-100",
"fr": "Affiche de territoire UPG-100",
"hi": "UPG-100 क्षेत्र पोस्टर",
"it": "Manifesto di territorio UPG-100",
"pt": "Cartaz de território UPG-100",
"ro": "Afiș de teritoriu UPG-100",
"ru": "Плакат территории UPG-100",
"zh": "UPG-100 区域海报"
},
"upg100-territory-poster.3x4": {
"ar": "ملصق منطقة UPG100",
"de": "UPG100-Gebietsplakat",
"en": "UPG100 zone poster",
"es": "Cartel de zona UPG100",
"fr": "Affiche de zone UPG100",
"hi": "UPG100 क्षेत्र का पोस्टर",
"it": "Poster di zona UPG100",
"pt": "Cartaz de zona UPG100",
"ro": "Poster de zonă UPG100",
"ru": "Плакат зоны UPG100",
"zh": "UPG100 区域海报(单张)"
"ar": "ملصق منطقة UPG-100",
"de": "UPG-100-Gebietsplakat",
"en": "UPG-100 territory poster",
"es": "Cartel de territorio UPG-100",
"fr": "Affiche de territoire UPG-100",
"hi": "UPG-100 क्षेत्र पोस्टर",
"it": "Manifesto di territorio UPG-100",
"pt": "Cartaz de território UPG-100",
"ro": "Afiș de teritoriu UPG-100",
"ru": "Плакат территории UPG-100",
"zh": "UPG-100 区域海报"
}
},
"file": "poster-wall.vue",
@ -1112,7 +1112,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-lGHqz/3puE+d+OsmZgC+TmlNYspuG9ifVaIyxyzGyAFf6cXh9wDwSKj9bFvZY4Fo\" 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-RoUFVUuNTZd9S6IGDGEbW7D975a4q6EFiZH4xdye1ggd3bbArVu2MT3tBYjXVWiD\" 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>"
}
},
{
@ -1128,8 +1128,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-lGHqz/3puE+d+OsmZgC+TmlNYspuG9ifVaIyxyzGyAFf6cXh9wDwSKj9bFvZY4Fo\" 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-lGHqz/3puE+d+OsmZgC+TmlNYspuG9ifVaIyxyzGyAFf6cXh9wDwSKj9bFvZY4Fo\" 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-RoUFVUuNTZd9S6IGDGEbW7D975a4q6EFiZH4xdye1ggd3bbArVu2MT3tBYjXVWiD\" 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-RoUFVUuNTZd9S6IGDGEbW7D975a4q6EFiZH4xdye1ggd3bbArVu2MT3tBYjXVWiD\" 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>"
}
},
{
@ -1145,8 +1145,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-lGHqz/3puE+d+OsmZgC+TmlNYspuG9ifVaIyxyzGyAFf6cXh9wDwSKj9bFvZY4Fo\" 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-lGHqz/3puE+d+OsmZgC+TmlNYspuG9ifVaIyxyzGyAFf6cXh9wDwSKj9bFvZY4Fo\" 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-RoUFVUuNTZd9S6IGDGEbW7D975a4q6EFiZH4xdye1ggd3bbArVu2MT3tBYjXVWiD\" 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-RoUFVUuNTZd9S6IGDGEbW7D975a4q6EFiZH4xdye1ggd3bbArVu2MT3tBYjXVWiD\" 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>"
}
}
]
@ -1219,7 +1219,7 @@
"bundle": "plugin-studio",
"scriptUrl": "./plugin-studio/plugin-studio.js",
"output": "dx-maps/plugin-studio/plugin-studio.js",
"integrity": "sha384-KH5r1cFKUtfzOiE5iRdTbaoHYP27HquWyUNX0gjnVai5MfNzwUiJI2kBaStmGuIY",
"integrity": "sha384-fUBGsSNbCdWEJ6QNmeOJf78N88LmjL7b5ktyt11xm5iSZxM/u2xwNdPzHaT2A6Dz",
"elements": [
"plugin-studio"
],
@ -1250,7 +1250,7 @@
"scriptUrl": "./plugin-studio/plugin-studio.js",
"embed": {
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/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=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-KH5r1cFKUtfzOiE5iRdTbaoHYP27HquWyUNX0gjnVai5MfNzwUiJI2kBaStmGuIY\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-fUBGsSNbCdWEJ6QNmeOJf78N88LmjL7b5ktyt11xm5iSZxM/u2xwNdPzHaT2A6Dz\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
},
"instances": [
{
@ -1277,7 +1277,7 @@
"bundle": "poster-design",
"scriptUrl": "./poster-design/poster-design.js",
"output": "dx-maps/poster-design/poster-design.js",
"integrity": "sha384-5r/RtSGqs1AaM+gimpy9hYjCwR+fzjHyYX617dMwaFWhydz6dlt09TOyb+i/78Vo",
"integrity": "sha384-hiRS9PFXm3IHABow+MMmBh4VYVldRxUQSnx7b8wh3H1UW4N+Dq22fTV/oWGGvSP8",
"elements": [
"poster-design"
],
@ -1300,7 +1300,7 @@
"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-5r/RtSGqs1AaM+gimpy9hYjCwR+fzjHyYX617dMwaFWhydz6dlt09TOyb+i/78Vo\" 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>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-hiRS9PFXm3IHABow+MMmBh4VYVldRxUQSnx7b8wh3H1UW4N+Dq22fTV/oWGGvSP8\" 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>"
}
},
{
@ -1316,7 +1316,7 @@
"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-5r/RtSGqs1AaM+gimpy9hYjCwR+fzjHyYX617dMwaFWhydz6dlt09TOyb+i/78Vo\" 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>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-hiRS9PFXm3IHABow+MMmBh4VYVldRxUQSnx7b8wh3H1UW4N+Dq22fTV/oWGGvSP8\" 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>"
}
},
{
@ -1332,7 +1332,7 @@
"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-5r/RtSGqs1AaM+gimpy9hYjCwR+fzjHyYX617dMwaFWhydz6dlt09TOyb+i/78Vo\" 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>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-hiRS9PFXm3IHABow+MMmBh4VYVldRxUQSnx7b8wh3H1UW4N+Dq22fTV/oWGGvSP8\" 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>"
}
}
]
@ -1341,7 +1341,7 @@
"bundle": "poster-sections",
"scriptUrl": "./poster-sections/poster-sections.js",
"output": "dx-maps/poster-sections/poster-sections.js",
"integrity": "sha384-yTqSEf7W1guaDdfAfm5AC3H1dG2wOasVHQdVjnV3XhFbXfukPq3wJokwLOrK4M4j",
"integrity": "sha384-vjNpH6iKZVNk0Y3j/vJUjy8iINx33MfiIbiwOzPvgUFKmrJg00vRCCbavMPr2rxK",
"elements": [
"poster-sections"
],
@ -1360,7 +1360,7 @@
"scriptUrl": "./poster-sections/poster-sections.js",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/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-sections/poster-sections.js\" integrity=\"sha384-yTqSEf7W1guaDdfAfm5AC3H1dG2wOasVHQdVjnV3XhFbXfukPq3wJokwLOrK4M4j\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-sections profile-config='{\"profile\":\"poster-sections\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-sections>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-sections/poster-sections.js\" integrity=\"sha384-vjNpH6iKZVNk0Y3j/vJUjy8iINx33MfiIbiwOzPvgUFKmrJg00vRCCbavMPr2rxK\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-sections profile-config='{\"profile\":\"poster-sections\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-sections>"
},
"instances": [
{
@ -1505,7 +1505,7 @@
"bundle": "posterverse",
"scriptUrl": "./posterverse/posterverse.js",
"output": "dx-maps/posterverse/posterverse.js",
"integrity": "sha384-SdRnjbGFsb/972hqwXtINbk7EFAAC++BUwdTv+qfQyBupa87B/ofj5uDzys/Rvi+",
"integrity": "sha384-1jvCHQrT11Mixml/KvdnT44djTWvIe6TjNk2vU66hwtF4mefgVCSIRhMwXukYAqZ",
"elements": [
"posterverse-app"
],
@ -1532,7 +1532,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-SdRnjbGFsb/972hqwXtINbk7EFAAC++BUwdTv+qfQyBupa87B/ofj5uDzys/Rvi+\" 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-1jvCHQrT11Mixml/KvdnT44djTWvIe6TjNk2vU66hwtF4mefgVCSIRhMwXukYAqZ\" 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": [
{
@ -1589,7 +1589,7 @@
"bundle": "prayer-walk-planner",
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
"output": "dx-maps/prayer-walk-planner/prayer-walk-planner.js",
"integrity": "sha384-6nj6SNKsRM5cmVVUmPgIAKe8ZSejxZew8HheG0P4rzyiUYdLrPVosfJJbrUypNiY",
"integrity": "sha384-7s2EGetAPnCoQ/rlga98sObVliNAk3lsWcVXt7hkQo5hQPKMLz4KVMWQpOFtxtf/",
"elements": [
"prayer-walk-planner"
],
@ -1607,8 +1607,8 @@
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
"embed": {
"iframe": "<iframe src=\"./prayer-walk-planner/prayer-walk-planner/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=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-6nj6SNKsRM5cmVVUmPgIAKe8ZSejxZew8HheG0P4rzyiUYdLrPVosfJJbrUypNiY\" 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<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>",
"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=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-6nj6SNKsRM5cmVVUmPgIAKe8ZSejxZew8HheG0P4rzyiUYdLrPVosfJJbrUypNiY\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
"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=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-7s2EGetAPnCoQ/rlga98sObVliNAk3lsWcVXt7hkQo5hQPKMLz4KVMWQpOFtxtf/\" 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<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>",
"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=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-7s2EGetAPnCoQ/rlga98sObVliNAk3lsWcVXt7hkQo5hQPKMLz4KVMWQpOFtxtf/\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
}
}
]
@ -1617,7 +1617,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-TzpyeMEeBj+L99cLsTKN2X27VxqYVTMqRdnxct/qhxQ5KrNOkQw7yJUGgaepqrex",
"integrity": "sha384-qe3f3jw8Yu3fdfWb2RGAQF49vZxIPyLoVgFxp0D8nKFfwpHqd3c36JhNI/g1Vxww",
"elements": [
"regions-dashboard"
],
@ -1640,8 +1640,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-TzpyeMEeBj+L99cLsTKN2X27VxqYVTMqRdnxct/qhxQ5KrNOkQw7yJUGgaepqrex\" 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-TzpyeMEeBj+L99cLsTKN2X27VxqYVTMqRdnxct/qhxQ5KrNOkQw7yJUGgaepqrex\" 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-qe3f3jw8Yu3fdfWb2RGAQF49vZxIPyLoVgFxp0D8nKFfwpHqd3c36JhNI/g1Vxww\" 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-qe3f3jw8Yu3fdfWb2RGAQF49vZxIPyLoVgFxp0D8nKFfwpHqd3c36JhNI/g1Vxww\" 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>"
}
},
{
@ -1657,7 +1657,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-TzpyeMEeBj+L99cLsTKN2X27VxqYVTMqRdnxct/qhxQ5KrNOkQw7yJUGgaepqrex\" 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-qe3f3jw8Yu3fdfWb2RGAQF49vZxIPyLoVgFxp0D8nKFfwpHqd3c36JhNI/g1Vxww\" 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>"
}
},
{
@ -1673,8 +1673,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-TzpyeMEeBj+L99cLsTKN2X27VxqYVTMqRdnxct/qhxQ5KrNOkQw7yJUGgaepqrex\" 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-TzpyeMEeBj+L99cLsTKN2X27VxqYVTMqRdnxct/qhxQ5KrNOkQw7yJUGgaepqrex\" 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-qe3f3jw8Yu3fdfWb2RGAQF49vZxIPyLoVgFxp0D8nKFfwpHqd3c36JhNI/g1Vxww\" 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-qe3f3jw8Yu3fdfWb2RGAQF49vZxIPyLoVgFxp0D8nKFfwpHqd3c36JhNI/g1Vxww\" 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>"
}
}
]
@ -1683,7 +1683,7 @@
"bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "dx-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-szV59QxctTn0xXiZHAyXVBUP1QY54HpVGtClXc58F+X1JOYmzQUStEFQWi6UAl3/",
"integrity": "sha384-QXxsY2mQTDSHzPA/PliyvqmkiiaM53CiNEYx0aREj2wgRHD/bDGJfhsW4v5R1Ki0",
"elements": [
"research-dashboard-map"
],
@ -1702,8 +1702,8 @@
"scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-szV59QxctTn0xXiZHAyXVBUP1QY54HpVGtClXc58F+X1JOYmzQUStEFQWi6UAl3/\" 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<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-szV59QxctTn0xXiZHAyXVBUP1QY54HpVGtClXc58F+X1JOYmzQUStEFQWi6UAl3/\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-QXxsY2mQTDSHzPA/PliyvqmkiiaM53CiNEYx0aREj2wgRHD/bDGJfhsW4v5R1Ki0\" 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<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-QXxsY2mQTDSHzPA/PliyvqmkiiaM53CiNEYx0aREj2wgRHD/bDGJfhsW4v5R1Ki0\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
},
"bundle": "research-dashboard"
}
@ -1808,7 +1808,7 @@
"bundle": "upg-neighbor",
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"output": "dx-maps/upg-neighbor/upg-neighbor.js",
"integrity": "sha384-g6NBHTZ945HuiOxk/eGRpMe64qQcAVdFjn0KMeclyAZCwZ3NlIyaKfsOCq55uJab",
"integrity": "sha384-dnPko47/tcfCHCOlPLCBYFTKcZJme8/Gk/TFXHaOcG8RD0m90YODMHsjpTHvqBBC",
"elements": [
"upg-neighbor"
],
@ -1827,8 +1827,8 @@
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"embed": {
"iframe": "<iframe src=\"./upg-neighbor/upg-neighbor/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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-g6NBHTZ945HuiOxk/eGRpMe64qQcAVdFjn0KMeclyAZCwZ3NlIyaKfsOCq55uJab\" 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<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>",
"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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-g6NBHTZ945HuiOxk/eGRpMe64qQcAVdFjn0KMeclyAZCwZ3NlIyaKfsOCq55uJab\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
"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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-dnPko47/tcfCHCOlPLCBYFTKcZJme8/Gk/TFXHaOcG8RD0m90YODMHsjpTHvqBBC\" 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<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>",
"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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-dnPko47/tcfCHCOlPLCBYFTKcZJme8/Gk/TFXHaOcG8RD0m90YODMHsjpTHvqBBC\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
},
"instances": [
{

File diff suppressed because one or more lines are too long

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../doxa-research-map.js" integrity="sha384-w8mEGeIr5DTaQEIrfMaga7c8AOzBfk5BCFQWhhBLStfA1x+8/WPn0W8FVDM77tX+" crossorigin="anonymous"></script>
<script src="../doxa-research-map.js" integrity="sha384-B7rrEPAoDC9HdBjrUUdCZumSu+y/t/j0H8PJ7Q9s7nTBcYY9TeJfSjr6dRAK9a/V" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -72,7 +72,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../../doxa-research-map.js" integrity="sha384-w8mEGeIr5DTaQEIrfMaga7c8AOzBfk5BCFQWhhBLStfA1x+8/WPn0W8FVDM77tX+" crossorigin="anonymous"></script>
<script src="../../doxa-research-map.js" integrity="sha384-B7rrEPAoDC9HdBjrUUdCZumSu+y/t/j0H8PJ7Q9s7nTBcYY9TeJfSjr6dRAK9a/V" 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

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dxm-simple-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-dVdk9Dyd0DZ92VoOULWQFVRqexr7zKwDyvy9GkGXHe0oGo3tqS+xR1pc04QP/B0L" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-m1zUFDjAPVm/dldyOUCh8fR061OIAUBl5Iclo0iL3wy6P5+wqCeEioL0Dmck3Opn" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dxm-simple-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-dVdk9Dyd0DZ92VoOULWQFVRqexr7zKwDyvy9GkGXHe0oGo3tqS+xR1pc04QP/B0L" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-m1zUFDjAPVm/dldyOUCh8fR061OIAUBl5Iclo0iL3wy6P5+wqCeEioL0Dmck3Opn" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dxm-simple-map> custom element. -->
<script src="../doxa-simple-map.js" integrity="sha384-dVdk9Dyd0DZ92VoOULWQFVRqexr7zKwDyvy9GkGXHe0oGo3tqS+xR1pc04QP/B0L" crossorigin="anonymous"></script>
<script src="../doxa-simple-map.js" integrity="sha384-m1zUFDjAPVm/dldyOUCh8fR061OIAUBl5Iclo0iL3wy6P5+wqCeEioL0Dmck3Opn" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dxm-simple-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-dVdk9Dyd0DZ92VoOULWQFVRqexr7zKwDyvy9GkGXHe0oGo3tqS+xR1pc04QP/B0L" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-m1zUFDjAPVm/dldyOUCh8fR061OIAUBl5Iclo0iL3wy6P5+wqCeEioL0Dmck3Opn" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../gospel-internet.js" integrity="sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc" crossorigin="anonymous"></script>
<script src="../gospel-internet.js" integrity="sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-cYEFAJDnY5U2DTUhnYFCCSG7rUsqh1c8nzExu1GePI0mIKdFZU3vQ/zKHkSBHdvc" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-wbljRqxErJtdKSc2q8bFZ4voFf9vN9+8EM6xJOewT8DHnhKvX9kJiEVyKT7kaqFL" 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

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -82,7 +82,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q" 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

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-bUYx/HIJrMOYevH8JQVp4qeZnLx9cHrT9ZbuuNTGzVy32DXCL1lnIwABy8f2Bi4M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-rJHcQHAnoHTreo0lHwHsluv18kL5wSjRMghVgQ0OR0BE7eLRA+jEIftd88xKoC5q" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -24,6 +24,6 @@
</head>
<body>
<plugin-studio tree-base="."></plugin-studio>
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-KH5r1cFKUtfzOiE5iRdTbaoHYP27HquWyUNX0gjnVai5MfNzwUiJI2kBaStmGuIY" crossorigin="anonymous"></script>
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-fUBGsSNbCdWEJ6QNmeOJf78N88LmjL7b5ktyt11xm5iSZxM/u2xwNdPzHaT2A6Dz" crossorigin="anonymous"></script>
</body>
</html>

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