Merge remote-tracking branch 'origin/development' into upg100/batch-tap
# Conflicts: # public/dx-maps/doxa-maps-spa/manifest.json # public/dx-maps/manifest.json # public/dx-maps/my-upg-100-list/my-upg-100-list.js # public/dx-maps/my-upg-100-list/poster-wall/index.html # public/dx-maps/my-upg-100-list/upg-100-list/index.html # public/dx-maps/my-upg-100-list/upg-100-mfe/index.html
138
README.md
|
|
@ -6,7 +6,9 @@ Made by Vibe Coders for Vibe Coders to prepare the way of the Lord and make His
|
|||
|
||||
## Bottom line up front
|
||||
|
||||
- **Why:** dx-maps exists to give disciples, churches and leaders what they need to bless the
|
||||
- **Why:** To Bless the Nations
|
||||
|
||||
- **Great Commission:** dx-maps exists to give disciples, churches and leaders what they need to bless the
|
||||
nations. We believe the deeper answer to the Great Commission is indigenous disciples, churches
|
||||
and leaders multiplying among their own peoples and blessing them, so that the Commission is
|
||||
fulfilled through love. The maps serve that in three ways: praying for the unreached peoples of
|
||||
|
|
@ -32,6 +34,12 @@ Made by Vibe Coders for Vibe Coders to prepare the way of the Lord and make His
|
|||
|
||||
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.
|
||||
|
||||
Every poster is in Posterverse (`plugin-designs/posterverse/`): the UPG 100 List, 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
|
||||
|
||||
**Write a map once in `plugin-designs/<your-map>/`, run one build, and drop it on any site** —
|
||||
|
|
@ -62,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, one shepherd, one working `development`** | [agent swarm](#build-it-with-an-agent-swarm) |
|
||||
| **One owner per plugin, a second reader for every change, one landing at a time** | [agent swarm](#build-it-with-an-agent-swarm) |
|
||||
|
||||
## For vibe coders: build, open the Studio, embed a map
|
||||
|
||||
|
|
@ -169,11 +177,11 @@ the Research Gallery, which DX Life publishes as **Deep Research**. An eighth pr
|
|||
|
||||
| Capability | Example | iframe | Mobile app | JavaScript embed | WordPress / Disciple Tools | Desktop |
|
||||
|---|---|:-:|:-:|:-:|:-:|:-:|
|
||||
| **5 — app shell** | Research Gallery | ✅ | ✅ | ✅ | 🧪 | 🧪 |
|
||||
| **4 — composite** | UPG-100 List | ✅ | ✅ | ✅ | 🧪 | 🧪 |
|
||||
| **3 — multi-view** | research map | ✅ | ✅ | ✅ | 🧪 | 🧪 |
|
||||
| **2 — presets** | Prayer Progress | ✅ | ✅ | ✅ | 🧪 | 🧪 |
|
||||
| **1 — single element** | all people groups | ✅ | ✅ | ✅ | 🧪 | 🧪 |
|
||||
| **5 — app shell** | Research Gallery | ✅ | ✅ | ✅ | 🧪 | ✅ |
|
||||
| **4 — composite** | UPG-100 List | ✅ | ✅ | ✅ | 🧪 | ✅ |
|
||||
| **3 — multi-view** | research map | ✅ | ✅ | ✅ | 🧪 | ✅ |
|
||||
| **2 — presets** | Prayer Progress | ✅ | ✅ | ✅ | 🧪 | ✅ |
|
||||
| **1 — single element** | all people groups | ✅ | ✅ | ✅ | 🧪 | ✅ |
|
||||
|
||||
<sub>✅ works today · 🧪 proven, not yet in production. Every plugin builds into the same folder, and that folder is what all five methods ship — so a capability never limits where a plugin can go.</sub>
|
||||
|
||||
|
|
@ -425,9 +433,9 @@ put it* and *how a page shows it*.
|
|||
|
||||
| # | Output | What it is | Status |
|
||||
|---|---|---|---|
|
||||
| **1** | **The web folder** `public/dx-maps/` | every map, each embeddable on its own, all working together — 1205 files, 154 MiB (about 160 MB) | ✅ production |
|
||||
| **1** | **The web folder** `public/dx-maps/` | every map, each embeddable on its own, all working together — 1367 files, 163 MiB (about 170 MB) | ✅ production |
|
||||
| **2** | **Mobile app** | the same folder, wrapped as an Android app by Capacitor | ✅ built |
|
||||
| **3** | **Desktop app** | the same folder, wrapped as a desktop app by Tauri | 🧪 proven, not yet in production |
|
||||
| **3** | **Desktop app** | the same folder, wrapped as a desktop app by Tauri | ✅ works today |
|
||||
|
||||
### Output 1 — the web folder
|
||||
|
||||
|
|
@ -581,16 +589,16 @@ bun run phone # builds the app and installs it on a phone or emulator
|
|||
|
||||
</details>
|
||||
|
||||
### Output 3 — the desktop app 🧪 proven, not yet in production
|
||||
### Output 3 — the desktop app ✅ works today
|
||||
|
||||
**We got the JAMstack map app running as a desktop app.**
|
||||
|
||||

|
||||

|
||||
|
||||
**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)).
|
||||
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary><b>More about the desktop app</b></summary>
|
||||
|
|
@ -687,11 +695,62 @@ your own page, set `"locale"` in its `profile-config`. Arabic reads right to lef
|
|||
## Build it with an agent swarm
|
||||
|
||||
**Many AI agents can build many plugins at once without breaking each other's work.** Every plugin
|
||||
has one agent building it, and one shepherd agent makes sure every agent does its job well. All the
|
||||
plugins live in one project with one shared library, and changes are added to `development`, the shared
|
||||
working version, one at a time. `main` is the production version, and it moves only when a release is published.
|
||||
has one agent that owns it, and changes are added to `development`, the shared working version, one
|
||||
at a time. `main` is the production version: it moves only when you publish a release.
|
||||
|
||||

|
||||
**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. Roles such as a coordinator that puts the landings in order, or a
|
||||
gate-keeper that reads each change before it lands, are jobs your harness gives to its agents.
|
||||
|
||||

|
||||
|
||||
<sub>[Open the diagram full size](documentation/readme-images/delegation-model.png): on a phone its small text needs it.</sub>
|
||||
|
||||
### Set up the branch strategy
|
||||
|
||||
1. **`main` is production.** It moves only when you publish a release (step 7).
|
||||
2. **`development` is the shared working branch.** Create it once, from `main`:
|
||||
```bash
|
||||
git fetch origin && git switch -c development origin/main && git push -u origin development
|
||||
```
|
||||
3. **One branch and worktree per plugin agent**, from `origin/development`, 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
|
||||
cd ../dx-maps-regions && bun install --frozen-lockfile
|
||||
```
|
||||
4. **One preview server per agent, each on its own port:**
|
||||
```bash
|
||||
bun x vite --port 5301 --strictPort # the next agent uses 5302, and so on
|
||||
```
|
||||
5. **Land one at a time, through a lock.** Catch up with `development`, run the checks, and push only
|
||||
if nothing fails (`gates:pre` exits non-zero when a check fails, so the push never runs then):
|
||||
```bash
|
||||
flock /tmp/dx-maps-development.lock sh -c '
|
||||
git fetch origin && git rebase origin/development &&
|
||||
bun run gates:pre && git push origin HEAD:development'
|
||||
```
|
||||
`flock` guards one machine. Agents on several machines need a shared lock, such as one your
|
||||
harness provides.
|
||||
6. **One agent owns the full build**; the others build only their own plugin:
|
||||
```bash
|
||||
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
|
||||
```
|
||||
7. **Release:** fast-forward `main` to a `development` 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 merge --ff-only <development-commit> # a commit that passed the checks
|
||||
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
|
||||
([how to deploy](./documentation/DEPLOY.md)).
|
||||
|
||||
How to contribute, and the bar for shared code: [documentation/CONTRIBUTING.md](./documentation/CONTRIBUTING.md).
|
||||
Diagram source: [`delegation-model.svg`](./documentation/readme-images/src/delegation-model.svg).
|
||||
|
||||
<details>
|
||||
<summary><b>What problem does this solve? (in plain words)</b></summary>
|
||||
|
|
@ -702,15 +761,17 @@ map breaks at once**. And when several agents work in the same project at the sa
|
|||
each other — two agents edit the same file, one change quietly erases another, or half-finished work
|
||||
lands in the version everyone else is using.
|
||||
|
||||
So the project works like a busy kitchen with one head chef:
|
||||
So the project works like a busy kitchen:
|
||||
|
||||
1. **Each agent works in its own copy of the project** — its own counter, so nobody bumps anybody.
|
||||
2. **Before adding its work, each agent catches up**: it pulls in everyone else's latest work and
|
||||
fixes any clashes with its own. The agent fixes the clash, not the next person.
|
||||
3. **Only one agent adds work at a time.** They wait in a line (the *merge queue*), the checks run,
|
||||
and only then does the change go into `development`. `development` always works.
|
||||
4. **A shepherd agent watches everyone**: plugins built in the studio, shared parts really shared,
|
||||
and every change going through the line.
|
||||
1. **Each agent works in its own branch and copy of the project** — its own counter, so nobody bumps
|
||||
anybody. One owner per plugin, and one per shared file.
|
||||
2. **Only one change lands at a time.** Whoever holds the *landing lock* first catches up with
|
||||
everyone else's latest work, fixes any clash itself and runs every check; the change goes into
|
||||
`development` only if nothing fails. `development` always works.
|
||||
3. **Someone keeps order.** A coordinator can put the landings in order, and a gate-keeper can read
|
||||
each change by its content before it lands. Your harness gives these jobs to its agents.
|
||||
4. **`main` moves only when you publish a release**, after the checks pass on that exact version.
|
||||
A preview can follow `development`; the published site follows `main`.
|
||||
|
||||
That's how many plugins get built at the same time, in one project, without the maps breaking.
|
||||
|
||||
|
|
@ -727,31 +788,8 @@ That's how many plugins get built at the same time, in one project, without the
|
|||
| **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 |
|
||||
| **rebase** | catching your work up with everyone else's latest before adding it |
|
||||
| **merge queue** | the line agents wait in, so only one change goes into `main` at a time |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>The commands (for the curious)</b></summary>
|
||||
|
||||
```bash
|
||||
# one private copy (worktree) + one agent per plugin
|
||||
git worktree add -b regions/work ../dx-maps-regions origin/development
|
||||
git worktree add -b upg100/work ../dx-maps-upg100 origin/development
|
||||
|
||||
# one preview server per agent, each on its own port
|
||||
bun x vite --port 5301 --strictPort
|
||||
|
||||
# add work through the line: one at a time, caught up first, checks must pass
|
||||
flock /tmp/dx-maps-development.lock sh -c '
|
||||
git fetch origin && git rebase origin/development &&
|
||||
bun run gates:pre && git push origin HEAD:development'
|
||||
```
|
||||
|
||||
One agent owns the full build (it rebuilds everyone's output); the others check just their own
|
||||
plugin with `BUNDLE=<name> bun x vite build`. How to contribute, and the bar for shared code:
|
||||
[documentation/CONTRIBUTING.md](./documentation/CONTRIBUTING.md). Diagram source:
|
||||
[`delegation-model.svg`](./documentation/readme-images/src/delegation-model.svg).
|
||||
| **landing lock** | only one change lands on `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 |
|
||||
|
||||
</details>
|
||||
|
||||
|
|
@ -773,7 +811,7 @@ and choose `"provider": "mapbox"` (see *No Mapbox token?* below). For Rudder's v
|
|||
`bash backend/rudder-engine/setup-runtime.sh` once; without Go, everything except voice works.
|
||||
|
||||
```bash
|
||||
git clone --depth 1 https://git.nuxtbase.app/doxa-open/doxa-maps.git # latest code only: seconds, not hundreds of MB of history
|
||||
git clone --depth 1 <your-repo-url> # latest code only: seconds, not hundreds of MB of history
|
||||
cd doxa-maps
|
||||
bun install --frozen-lockfile
|
||||
cp .env.example .env.local # optional: paste your pk. token after VITE_MAPBOX_TOKEN= (.env works too; .env.local wins)
|
||||
|
|
|
|||
BIN
data/posters/india-an.webp
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
data/posters/india-ap.webp
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
data/posters/india-ar.webp
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
data/posters/india-as.webp
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
data/posters/india-br.webp
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
data/posters/india-ch.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
data/posters/india-ct.webp
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
data/posters/india-dh.webp
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
data/posters/india-dl.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
data/posters/india-ga.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
data/posters/india-gj.webp
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
data/posters/india-hp.webp
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
data/posters/india-hr.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
data/posters/india-jh.webp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
data/posters/india-jk.webp
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
data/posters/india-ka.webp
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
data/posters/india-kl.webp
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
data/posters/india-la.webp
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
data/posters/india-ld.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
data/posters/india-mh.webp
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
data/posters/india-ml.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
data/posters/india-mn.webp
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
data/posters/india-mp.webp
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
data/posters/india-or.webp
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
data/posters/india-pb.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
data/posters/india-py.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
data/posters/india-rj.webp
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
data/posters/india-sk.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
data/posters/india-tg.webp
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
data/posters/india-tn.webp
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
data/posters/india-tr.webp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
data/posters/india-up.webp
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
data/posters/india-ut.webp
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
data/posters/india-wb.webp
Normal file
|
After Width: | Height: | Size: 146 KiB |
|
|
@ -42,7 +42,7 @@ const refuse = (msg) => (PUBLIC
|
|||
|
||||
// fetch() hands back the body DECOMPRESSED, so the upstream Content-Encoding and Content-Length no longer describe
|
||||
// what is sent on: forwarding them made browsers try to gunzip plain HTML, and PocketBase's dashboard never rendered
|
||||
// (rega, 2026-09-29). And PocketBase redirects to its own root (/_ -> /_/), which is put back under the mount (/pb/_/).
|
||||
// (measured 2026-09-29). And PocketBase redirects to its own root (/_ -> /_/), which is put back under the mount (/pb/_/).
|
||||
// A backend path as the backend will read it: percent-decoded, runs of slashes collapsed. The admin gates run on
|
||||
// THIS form, so /pb/%5F/ (PocketBase decodes it to /_/) meets the same check as /pb/_/ (studio, 2026-09-29).
|
||||
// Encoded slashes or backslashes and undecodable input are refused: nothing legitimate sends them.
|
||||
|
|
@ -179,7 +179,7 @@ Bun.serve({
|
|||
const bp = backendPath(p)
|
||||
if (bp === null) return text('bad path', 400)
|
||||
if (!p.startsWith('/api-prod/api/') || !bp.startsWith('/api-prod/api/')) return text('only /api/ is public', 403)
|
||||
// As on /pb: the superuser and every _system collection stay closed. No public client uses them (dx-base, V2 review).
|
||||
// As on /pb: the superuser and every _system collection stay closed. No public client uses them (a platform security review).
|
||||
if (bp.startsWith('/api-prod/api/collections/_')) return text('for admins only', 403)
|
||||
return proxy(req, url, '/api-prod/', 8091)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ building ones run the live source with hot reload; the showing ones serve the **
|
|||
## 1. Your laptop — `bun run dev`
|
||||
|
||||
```bash
|
||||
git clone --depth 1 https://git.nuxtbase.app/doxa-open/doxa-maps.git
|
||||
git clone --depth 1 <your-repo-url>
|
||||
cd doxa-maps
|
||||
bun install --frozen-lockfile
|
||||
bun run dev # → http://localhost:5173/ is the Plugin Studio
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 816 KiB After Width: | Height: | Size: 858 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 38 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="2030" viewBox="0 0 1500 2030" font-family="Inter, 'Helvetica Neue', Arial, sans-serif">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="2086" viewBox="0 0 1500 2086" font-family="Inter, 'Helvetica Neue', Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#2563eb"/></marker>
|
||||
<marker id="aS" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#7c3aed"/></marker>
|
||||
|
|
@ -9,8 +9,8 @@
|
|||
<path id="agL" d="M 194,850 A 556,556 0 0 1 1306,850"/>
|
||||
<path id="mqL" d="M 679,884 A 71,71 0 0 0 821,884"/>
|
||||
</defs>
|
||||
<rect width="1500" height="2030" fill="#ffffff"/>
|
||||
<text x="750" y="54" text-anchor="middle" font-size="34" font-weight="800" fill="#0f172a">How dx-maps is built: one studio, one agent per plugin, one shepherd</text>
|
||||
<rect width="1500" height="2086" fill="#ffffff"/>
|
||||
<text x="750" y="54" text-anchor="middle" font-size="34" font-weight="800" fill="#0f172a">How dx-maps is built: one owner per plugin, one landing at a time</text>
|
||||
<circle cx="750" cy="850" r="540" fill="none" stroke="#2563eb" stroke-width="1.6" stroke-dasharray="3 7" opacity=".75"/>
|
||||
<circle cx="750" cy="850" r="355" fill="none" stroke="#15803d" stroke-width="2.2" opacity=".85"/>
|
||||
<circle cx="750" cy="850" r="606" fill="none" stroke="#7c3aed" stroke-width="2" stroke-dasharray="8 7" opacity=".6"/>
|
||||
|
|
@ -29,8 +29,8 @@
|
|||
<circle cx="750" cy="850" r="120" fill="#fef2f2" stroke="#dc2626" stroke-width="3" stroke-dasharray="10 6"/>
|
||||
<path id="mqT" d="M 646,850 A 104,104 0 0 1 854,850" fill="none"/>
|
||||
<path id="mqB" d="M 644,850 A 106,106 0 0 0 856,850" fill="none"/>
|
||||
<text font-size="13" font-weight="800" fill="#b91c1c" letter-spacing="1.6"><textPath href="#mqT" startOffset="50%" text-anchor="middle">MERGE QUEUE · GUARD</textPath></text>
|
||||
<text font-size="12" font-weight="700" fill="#b91c1c" letter-spacing="1.2"><textPath href="#mqB" startOffset="50%" text-anchor="middle">ONE AGENT AT A TIME</textPath></text>
|
||||
<text font-size="13" font-weight="800" fill="#b91c1c" letter-spacing="1.6"><textPath href="#mqT" startOffset="50%" text-anchor="middle">LANDING LOCK</textPath></text>
|
||||
<text font-size="12" font-weight="700" fill="#b91c1c" letter-spacing="1.2"><textPath href="#mqB" startOffset="50%" text-anchor="middle">ONE LANDING AT A TIME</textPath></text>
|
||||
<rect x="617" y="846" width="26" height="20" rx="4" fill="#dc2626"/>
|
||||
<path d="M 622,846 v -6 a 8,8 0 0 1 16,0 v 6" fill="none" stroke="#dc2626" stroke-width="3.5"/>
|
||||
<circle cx="630" cy="856" r="3" fill="#fff"/>
|
||||
|
|
@ -38,9 +38,9 @@
|
|||
<path d="M 862,846 v -6 a 8,8 0 0 1 16,0 v 6" fill="none" stroke="#dc2626" stroke-width="3.5"/>
|
||||
<circle cx="870" cy="856" r="3" fill="#fff"/>
|
||||
<circle cx="750" cy="850" r="72" fill="#166534" stroke="#14532d" stroke-width="3"/>
|
||||
<text x="750" y="852" text-anchor="middle" font-size="34" font-weight="800" fill="#fff">main</text>
|
||||
<text x="750" y="876" text-anchor="middle" font-size="12.5" fill="#dcfce7">the one branch</text>
|
||||
<text x="750" y="892" text-anchor="middle" font-size="12.5" fill="#dcfce7">always green</text>
|
||||
<text x="750" y="850" text-anchor="middle" font-size="18" font-weight="800" fill="#fff">development</text>
|
||||
<text x="750" y="872" text-anchor="middle" font-size="12.5" fill="#dcfce7">every change</text>
|
||||
<text x="750" y="888" text-anchor="middle" font-size="12.5" fill="#dcfce7">lands here</text>
|
||||
<text font-size="16" font-weight="800" fill="#15803d" letter-spacing="3"><textPath href="#plL" startOffset="50%" text-anchor="middle">PLUGINS</textPath></text>
|
||||
<text font-size="16" font-weight="800" fill="#2563eb" letter-spacing="3"><textPath href="#agL" startOffset="18%" text-anchor="middle">AGENTS</textPath></text>
|
||||
<line x1="862.1" y1="579.3" x2="822.7" y2="674.5" stroke="#15803d" stroke-width="1.4" opacity=".45"/>
|
||||
|
|
@ -117,67 +117,83 @@
|
|||
<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">roaming</text>
|
||||
<text x="543.4" y="364.1" text-anchor="middle" font-size="12" font-weight="700" fill="#155e75">agent</text>
|
||||
<circle cx="750" cy="244" r="60" fill="#f5f3ff" stroke="#7c3aed" stroke-width="3.5"/>
|
||||
<text x="750" y="251" text-anchor="middle" font-size="19" font-weight="800" fill="#4c1d95">shepherd</text>
|
||||
<text x="834" y="192" font-size="17" fill="#4c1d95">one shepherd makes sure all plugin agents</text>
|
||||
<text x="834" y="216" font-size="17" fill="#4c1d95">are doing their job building the plugins well</text>
|
||||
<text x="750" y="1502" text-anchor="middle" font-size="20" font-weight="800" fill="#1e3a8a">each plugin has one agent responsible for building it</text>
|
||||
<rect x="36" y="110" width="300" height="280" rx="14" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2"/>
|
||||
<circle cx="64" cy="144" r="12" fill="#166534" stroke="#14532d" stroke-width="2.5"/>
|
||||
<text x="88" y="149" font-size="15" fill="#334155">main — the one branch</text>
|
||||
<circle cx="64" cy="180" r="12" fill="#fef2f2" stroke="#dc2626" stroke-width="2.5"/>
|
||||
<text x="88" y="185" font-size="15" fill="#334155">merge queue — one at a time</text>
|
||||
<circle cx="64" cy="216" r="12" fill="#fffbeb" stroke="#d97706" stroke-width="2.5"/>
|
||||
<text x="88" y="221" font-size="15" fill="#334155">Plugin Studio — the one repo</text>
|
||||
<circle cx="64" cy="252" r="12" fill="#f0fdf4" stroke="#15803d" stroke-width="2.5"/>
|
||||
<text x="88" y="257" font-size="15" fill="#334155">plugin (badge = capability)</text>
|
||||
<circle cx="64" cy="288" r="12" fill="#ecfeff" stroke="#0891b2" stroke-width="2.5"/>
|
||||
<text x="88" y="293" font-size="15" fill="#334155">Experimental shelf</text>
|
||||
<circle cx="64" cy="324" r="12" fill="#eff6ff" stroke="#2563eb" stroke-width="2.5"/>
|
||||
<text x="88" y="329" font-size="15" fill="#334155">agent — builds one plugin</text>
|
||||
<circle cx="64" cy="360" r="12" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2.5"/>
|
||||
<text x="88" y="365" font-size="15" fill="#334155">shepherd — oversees all</text>
|
||||
<rect x="60" y="1536" width="1380" height="218" 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 main is protected — so many plugins can be built at once</text>
|
||||
<rect x="80.0" y="1602" width="410" height="130" rx="14" fill="#fff" stroke="#dc2626" stroke-width="2"/>
|
||||
<circle cx="116.0" cy="1638" r="20" fill="#dc2626"/>
|
||||
<text x="116.0" y="1645" text-anchor="middle" font-size="20" font-weight="800" fill="#fff">1</text>
|
||||
<text x="148.0" y="1645" font-size="20" font-weight="800" fill="#0f172a">Work in your own copy</text>
|
||||
<text x="102.0" y="1686" font-size="16" fill="#475569">each agent builds its own plugin,</text>
|
||||
<text x="102.0" y="1710" font-size="16" fill="#475569">in its own private copy of the project</text>
|
||||
<text x="517.5" y="1677" text-anchor="middle" font-size="30" font-weight="800" fill="#dc2626">→</text>
|
||||
<rect x="545.0" y="1602" width="410" height="130" rx="14" fill="#fff" stroke="#dc2626" stroke-width="2"/>
|
||||
<circle cx="581.0" cy="1638" r="20" fill="#dc2626"/>
|
||||
<text x="581.0" y="1645" text-anchor="middle" font-size="20" font-weight="800" fill="#fff">2</text>
|
||||
<text x="613.0" y="1645" font-size="20" font-weight="800" fill="#0f172a">Catch up, fix the clashes</text>
|
||||
<text x="567.0" y="1686" font-size="16" fill="#475569">pull in everyone else's latest work and</text>
|
||||
<text x="567.0" y="1710" font-size="16" fill="#475569">fix any clashes before adding yours</text>
|
||||
<text x="982.5" y="1677" text-anchor="middle" font-size="30" font-weight="800" fill="#dc2626">→</text>
|
||||
<rect x="1010.0" y="1602" width="410" height="130" rx="14" fill="#fff" stroke="#dc2626" stroke-width="2"/>
|
||||
<circle cx="1046.0" cy="1638" r="20" fill="#dc2626"/>
|
||||
<text x="1046.0" y="1645" text-anchor="middle" font-size="20" font-weight="800" fill="#fff">3</text>
|
||||
<text x="1078.0" y="1645" font-size="20" font-weight="800" fill="#0f172a">Add changes one at a time</text>
|
||||
<text x="1032.0" y="1686" font-size="16" fill="#475569">the queue lets ONE agent in at a time;</text>
|
||||
<text x="1032.0" y="1710" font-size="16" fill="#475569">the checks must pass, main stays working</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">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">gate-keeper</text>
|
||||
<text x="884" y="192" font-size="17" fill="#4c1d95">a coordinator puts the landings 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">development — shared branch</text>
|
||||
<circle cx="64" cy="174" r="11" fill="#fef2f2" stroke="#dc2626" stroke-width="2.5"/>
|
||||
<text x="88" y="179" font-size="15" fill="#334155">landing lock — one at a time</text>
|
||||
<circle cx="64" cy="206" r="11" fill="#fffbeb" stroke="#d97706" stroke-width="2.5"/>
|
||||
<text x="88" y="211" font-size="15" fill="#334155">Plugin Studio — the one repo</text>
|
||||
<circle cx="64" cy="238" r="11" fill="#f0fdf4" stroke="#15803d" stroke-width="2.5"/>
|
||||
<text x="88" y="243" font-size="15" fill="#334155">plugin (badge = capability)</text>
|
||||
<circle cx="64" cy="270" r="11" fill="#ecfeff" stroke="#0891b2" stroke-width="2.5"/>
|
||||
<text x="88" y="275" font-size="15" fill="#334155">Experimental shelf</text>
|
||||
<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">coordinator — orders landings</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">gate-keeper — checks landings</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 lands — 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"/>
|
||||
<circle cx="114.0" cy="1634" r="18" fill="#dc2626"/>
|
||||
<text x="114.0" y="1641" text-anchor="middle" font-size="19" font-weight="800" fill="#fff">1</text>
|
||||
<text x="142.0" y="1641" font-size="19" font-weight="800" fill="#0f172a">Own branch</text>
|
||||
<text x="100.0" y="1680" font-size="15" fill="#475569">each owner builds its plugin</text>
|
||||
<text x="100.0" y="1702" font-size="15" fill="#475569">in its own branch and copy;</text>
|
||||
<text x="100.0" y="1724" font-size="15" fill="#475569">one owner per shared file</text>
|
||||
<text x="405.0" y="1682" text-anchor="middle" font-size="26" font-weight="800" fill="#dc2626">→</text>
|
||||
<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">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 land</text>
|
||||
<text x="750.0" y="1682" text-anchor="middle" font-size="26" font-weight="800" fill="#dc2626">→</text>
|
||||
<rect x="770.0" y="1600" width="305" height="148" rx="14" fill="#fff" stroke="#dc2626" stroke-width="2"/>
|
||||
<circle cx="804.0" cy="1634" r="18" fill="#dc2626"/>
|
||||
<text x="804.0" y="1641" text-anchor="middle" font-size="19" font-weight="800" fill="#fff">3</text>
|
||||
<text x="832.0" y="1641" font-size="19" font-weight="800" fill="#0f172a">One at a time</text>
|
||||
<text x="790.0" y="1680" font-size="15" fill="#475569">in the lock: merge development,</text>
|
||||
<text x="790.0" y="1702" font-size="15" fill="#475569">rebuild, run every check;</text>
|
||||
<text x="790.0" y="1724" font-size="15" fill="#475569">push only if nothing fails</text>
|
||||
<text x="1095.0" y="1682" text-anchor="middle" font-size="26" font-weight="800" fill="#dc2626">→</text>
|
||||
<rect x="1115.0" y="1600" width="305" height="148" rx="14" fill="#fff" stroke="#dc2626" stroke-width="2"/>
|
||||
<circle cx="1149.0" cy="1634" r="18" fill="#dc2626"/>
|
||||
<text x="1149.0" y="1641" text-anchor="middle" font-size="19" font-weight="800" fill="#fff">4</text>
|
||||
<text x="1177.0" y="1641" font-size="19" font-weight="800" fill="#0f172a">Promote to main</text>
|
||||
<text x="1135.0" y="1680" font-size="15" fill="#475569">main is production: it moves</text>
|
||||
<text x="1135.0" y="1702" font-size="15" fill="#475569">only when the owner approves,</text>
|
||||
<text x="1135.0" y="1724" font-size="15" fill="#475569">after full checks on that version</text>
|
||||
<line x1="750" y1="956" x2="750" y2="1786" stroke="#334155" stroke-width="2.5" stroke-dasharray="2 5" opacity=".0"/>
|
||||
<rect x="60" y="1780" width="1380" height="230" rx="20" fill="#f8fafc" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="750" y="1824" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">main → ONE build → one web folder → three outputs</text>
|
||||
<rect x="80.0" y="1852" width="420" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/>
|
||||
<text x="290.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 1</text>
|
||||
<text x="290.0" y="1906" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">The web folder</text>
|
||||
<text x="290.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">iframe · JS embed · WordPress (for fun)</text>
|
||||
<rect x="190.0" y="1946" width="200" height="26" rx="13" fill="#15803d" opacity=".13"/>
|
||||
<text x="290.0" y="1964" text-anchor="middle" font-size="12" font-weight="800" fill="#15803d">PRODUCTION</text>
|
||||
<rect x="540.0" y="1852" width="420" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/>
|
||||
<text x="750.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 2</text>
|
||||
<text x="750.0" y="1906" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">Mobile app</text>
|
||||
<text x="750.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">the same folder, wrapped by Capacitor</text>
|
||||
<rect x="650.0" y="1946" width="200" height="26" rx="13" fill="#15803d" opacity=".13"/>
|
||||
<text x="750.0" y="1964" text-anchor="middle" font-size="12" font-weight="800" fill="#15803d">BUILT</text>
|
||||
<rect x="1000.0" y="1852" width="420" height="130" rx="14" fill="#fff" stroke="#d97706" stroke-width="2.5"/>
|
||||
<text x="1210.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 3</text>
|
||||
<text x="1210.0" y="1906" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">Desktop app</text>
|
||||
<text x="1210.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">the same folder, wrapped by Tauri</text>
|
||||
<rect x="1110.0" y="1946" width="200" height="26" rx="13" fill="#d97706" opacity=".13"/>
|
||||
<text x="1210.0" y="1964" text-anchor="middle" font-size="12" font-weight="800" fill="#d97706">PROVEN · NOT YET LIVE</text>
|
||||
<rect x="60" y="1812" width="1380" height="254" rx="20" fill="#f8fafc" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="750" y="1856" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">main → ONE build → one web folder → three outputs</text>
|
||||
<text x="750" y="1882" text-anchor="middle" font-size="16" fill="#475569">the dev preview follows development · the published site follows main</text>
|
||||
<rect x="80.0" y="1908" width="420" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/>
|
||||
<text x="290.0" y="1930" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 1</text>
|
||||
<text x="290.0" y="1962" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">The web folder</text>
|
||||
<text x="290.0" y="1988" text-anchor="middle" font-size="15" fill="#475569">iframe · JS embed · WordPress (for fun)</text>
|
||||
<rect x="190.0" y="2002" width="200" height="26" rx="13" fill="#15803d" opacity=".13"/>
|
||||
<text x="290.0" y="2020" text-anchor="middle" font-size="12" font-weight="800" fill="#15803d">PRODUCTION</text>
|
||||
<rect x="540.0" y="1908" width="420" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/>
|
||||
<text x="750.0" y="1930" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 2</text>
|
||||
<text x="750.0" y="1962" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">Mobile app</text>
|
||||
<text x="750.0" y="1988" text-anchor="middle" font-size="15" fill="#475569">the same folder, wrapped by Capacitor</text>
|
||||
<rect x="650.0" y="2002" width="200" height="26" rx="13" fill="#15803d" opacity=".13"/>
|
||||
<text x="750.0" y="2020" text-anchor="middle" font-size="12" font-weight="800" fill="#15803d">BUILT</text>
|
||||
<rect x="1000.0" y="1908" width="420" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/>
|
||||
<text x="1210.0" y="1930" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 3</text>
|
||||
<text x="1210.0" y="1962" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">Desktop app</text>
|
||||
<text x="1210.0" y="1988" text-anchor="middle" font-size="15" fill="#475569">the same folder, wrapped by Tauri</text>
|
||||
<rect x="1110.0" y="2002" width="200" height="26" rx="13" fill="#15803d" opacity=".13"/>
|
||||
<text x="1210.0" y="2020" text-anchor="middle" font-size="12" font-weight="800" fill="#15803d">WORKING</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -13,7 +13,7 @@
|
|||
> bundle `parameterized` (cap-2) no matter how light it looks, and a `meta.json` `type`
|
||||
> declares tiers 3/4/5.
|
||||
|
||||
Generated from 26 bundle(s).
|
||||
Generated from 27 bundle(s).
|
||||
|
||||
### One canonical exemplar per tier (generated — quote these)
|
||||
|
||||
|
|
@ -51,6 +51,8 @@ Generated from 26 bundle(s).
|
|||
| `gospel-resources` | `resources-map` | `single` |
|
||||
| `my-upg-100-list` | `poster-wall` | `single` |
|
||||
| `my-upg-100-list` | `upg-100-list` | `single` |
|
||||
| `poster-design` | `poster-sizes` | `single` |
|
||||
| `poster-design` | `poster-styles` | `single` |
|
||||
| `prayer-walk-planner` | `prayer-walk-planner` | `single` |
|
||||
| `regions-dashboard` | `region-dashboards` | `single` |
|
||||
| `regions-dashboard` | `regions-list` | `single` |
|
||||
|
|
@ -83,6 +85,7 @@ Generated from 26 bundle(s).
|
|||
| `dashboard-plus-grid` | `dashboard-plus-grid` | `nested-elements-mfe` |
|
||||
| `gospel-resources` | `gospel-dashboard` | `nested-elements-mfe` |
|
||||
| `my-upg-100-list` | `upg-100-mfe` | `nested-elements-mfe` |
|
||||
| `poster-design` | `poster-design-mfe` | `nested-elements-mfe` |
|
||||
| `regions-dashboard` | `regions-mfe` | `nested-elements-mfe` |
|
||||
| `research-map-plus-grid` | `research-map-plus-grid` | `nested-elements-mfe` |
|
||||
| `stack-demo` | `stack-demo` | `nested-elements-mfe` |
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ and a change here can break other maps.
|
|||
`import … from '@map/components/maps/RegionCountryMap.vue'`
|
||||
- **MapPrintProgress** — MapPrintProgress.vue — SHARED. The loading screen of a map print (mapPrintTemplate.js).
|
||||
`import … from '@map/components/poster/MapPrintProgress.vue'`
|
||||
- **PanelSheets** — PanelSheets.vue — SHARED. A preview of a large poster as the sheets it prints on: the sheets pulled apart, eac
|
||||
`import … from '@map/components/poster/PanelSheets.vue'`
|
||||
- **PosterDialog** — PosterDialog.vue — modal for previewing + rendering a poster export.
|
||||
`import … from '@map/components/poster/PosterDialog.vue'`
|
||||
- **PosterPreview** — PosterPreview.vue — scaled live preview of the poster composition.
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ const LINKS = [
|
|||
// Composites whose children deliberately exchange nothing. The reason is printed on every run.
|
||||
const NO_LINKS = {
|
||||
'stack-demo': 'a demonstration stack: page knobs only; dashboard-plus-grid is the linked version',
|
||||
'poster-design': 'two independent views of the poster templates (Styles, Sizes); neither pane drives the other',
|
||||
}
|
||||
|
||||
// ── D: late getCurrentInstance() sites already routed to an owner ─────────────────────────────
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@
|
|||
* owner; counting it as a pass would be false. It exits 75 NOT MEASURED instead, NAMING every
|
||||
* distinct blocked request path and the token's source, so the next reader can tell which
|
||||
* credential was in play without the gate ever printing it.
|
||||
* ✗ AN EARLIER VERSION CALLED THIS "TILESET ENTITLEMENT" AND THAT WAS WRONG. Measured 2026-09-29 by
|
||||
* rega on this gate's own token: /v4/ streets-v8 and country-boundaries both return 200, with no
|
||||
* Referer and from research.nuxtbase.app, maps-dev.nuxtbase.app, doxa.life, 127.0.0.1 and
|
||||
* ✗ AN EARLIER VERSION CALLED THIS "TILESET ENTITLEMENT" AND THAT WAS WRONG. Measured 2026-09-29 with
|
||||
* this gate's own token: /v4/ streets-v8 and country-boundaries both return 200, with no
|
||||
* Referer and from our research and maps-dev hosts, doxa.life, 127.0.0.1 and
|
||||
* example.com. The token is unrestricted and entitled. The old claim rested on three no-Referer
|
||||
* probes, which cannot distinguish a URL-restricted token from a missing entitlement. Whatever 403s
|
||||
* is a different request or a different token — which is exactly what the gate must now report
|
||||
|
|
@ -102,9 +102,9 @@ async function serve(dir) {
|
|||
// rather than FAILED.
|
||||
//
|
||||
// ✗ WHY THIS NO LONGER CLAIMS "ENTITLEMENT" (corrected 2026-09-29). It used to report a 403 here
|
||||
// as "tileset, not the embed", on the strength of three no-Referer probes. MEASURED since, by rega,
|
||||
// as "tileset, not the embed", on the strength of three no-Referer probes. MEASURED since,
|
||||
// with the very token this gate runs on: /v4/ streets-v8 AND country-boundaries return 200 — with
|
||||
// no Referer, and with research.nuxtbase.app, maps-dev.nuxtbase.app, doxa.life, 127.0.0.1 and
|
||||
// no Referer, and with our research and maps-dev hosts, doxa.life, 127.0.0.1 and
|
||||
// example.com. The token is unrestricted and entitled, so whatever 403s here is a DIFFERENT
|
||||
// request (a style, sprite, glyphs, a composite with terrain) or a different token than the one
|
||||
// tested. Three no-Referer cases cannot tell a URL-restricted token from a missing entitlement,
|
||||
|
|
@ -168,7 +168,7 @@ async function measure(page, url, provider) {
|
|||
: c.classList.contains('maplibregl-canvas') ? 'maplibre' : 'other'
|
||||
// ★ EVERY map, not the two this repo happens to publish. Reading only
|
||||
// window.__rcmMap/__rdashMap made 5 of the 7 production leaves report -1 and then FAIL as
|
||||
// "0 rendered features" even when they had drawn (measured by rega, 2026-09-29). The hook
|
||||
// "0 rendered features" even when they had drawn (measured 2026-09-29). The hook
|
||||
// installed before navigation collects every Map the page constructs, whatever it calls it.
|
||||
const maps = (window.__gateMaps || []).slice()
|
||||
const published = window.__rcmMap || window.__rdashMap || null
|
||||
|
|
|
|||
118
internal/check-panel-sheets.mjs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-panel-sheets — the shared panel geometry (library/components/poster/panelSheets.js). Headless.
|
||||
*
|
||||
* A poster too big for one sheet prints as several sheets, each trimmed edge laid over the next sheet's
|
||||
* join band. This pins:
|
||||
* A. the two multi-sheet sizes: 60x96 = 3 × 60×36 stacked, 6 in joins (the Bless 5 × 8 ft poster);
|
||||
* 66x66 = 2 × 36×66 side by side, 6 in join (India); and a single sheet has no join;
|
||||
* B. the sheets TILE the finished poster: what each sheet shows (itself minus the band the sheet before
|
||||
* covers) covers every inch once, no gap, no double;
|
||||
* C. sliceFor: with 'glue' the slices tile the poster; with 'bleed' neighbours repeat exactly joinIn;
|
||||
* D. the print marks: a 'glue' join prints the red band; a 'bleed' join prints only a guide (a solid band
|
||||
* would show through a misaligned trim); a screen preview always draws the red band; the trim line is on
|
||||
* every sheet but the last; every sheet has its "s / N" tag;
|
||||
* E. the Bless poster takes its sheets, bands and marks from this module (no private copy to drift): its
|
||||
* 5x8ft content bands equal the panels' content boxes, and blessPoster.js carries no glue band of its own.
|
||||
* Two-sided: each arm runs against a planted defect and must fail.
|
||||
* EXIT 0 pass · 1 fail
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const real = await import(pathToFileURL(join(ROOT, 'library/components/poster/panelSheets.js')).href)
|
||||
const bless = await import(pathToFileURL(join(ROOT, 'plugin-designs/wall-of-people-groups/src/blessPoster.js')).href)
|
||||
const blessSrc = readFileSync(join(ROOT, 'plugin-designs/wall-of-people-groups/src/blessPoster.js'), 'utf8')
|
||||
const eq = (a, b) => Math.abs(a - b) < 1e-9
|
||||
|
||||
const SIZES = {
|
||||
'60x96': { sheetW: 60, sheetH: 36, count: 3, direction: 'vertical', joinIn: 6, want: { w: 60, h: 96, at: [0, 30, 60] } },
|
||||
'66x66': { sheetW: 36, sheetH: 66, count: 2, direction: 'horizontal', joinIn: 6, want: { w: 66, h: 66, at: [0, 30] } },
|
||||
'36x36': { sheetW: 36, sheetH: 36, count: 1, direction: 'vertical', joinIn: 0, want: { w: 36, h: 36, at: [0] } },
|
||||
}
|
||||
|
||||
function run(M) {
|
||||
const errs = []
|
||||
let checked = 0
|
||||
for (const [key, z] of Object.entries(SIZES)) {
|
||||
for (const joinFill of ['glue', 'bleed']) {
|
||||
const P = M.planPanels({ ...z, borderIn: 2, joinFill })
|
||||
const V = P.direction === 'vertical', along = V ? P.sheetH : P.sheetW, total = V ? P.finished.h : P.finished.w
|
||||
// A. geometry
|
||||
if (!eq(P.finished.w, z.want.w) || !eq(P.finished.h, z.want.h)) errs.push(`A ${key}: finished ${P.finished.w} × ${P.finished.h}, want ${z.want.w} × ${z.want.h}`)
|
||||
if (P.sheets.length !== z.count) errs.push(`A ${key}: ${P.sheets.length} sheets, want ${z.count}`)
|
||||
P.sheets.forEach((sh, s) => {
|
||||
const off = V ? sh.at.y : sh.at.x
|
||||
if (!eq(off, z.want.at[s])) errs.push(`A ${key}: sheet ${s + 1} starts at ${off} in, want ${z.want.at[s]}`)
|
||||
if ((s > 0) !== !!sh.band) errs.push(`A ${key}: sheet ${s + 1} ${sh.band ? 'has' : 'lacks'} a join band`)
|
||||
if (sh.band && !eq(V ? sh.band.h : sh.band.w, z.joinIn)) errs.push(`A ${key}: sheet ${s + 1}'s band is not ${z.joinIn} in deep`)
|
||||
if (sh.trimmed !== (s < z.count - 1)) errs.push(`A ${key}: sheet ${s + 1} trimmed=${sh.trimmed}`)
|
||||
const c = sh.content, lead = V ? c.y0 : c.x0, trail = along - (V ? c.y1 : c.x1)
|
||||
if (!eq(lead, s === 0 ? 2 : z.joinIn) || !eq(trail, s === z.count - 1 ? 2 : 0)) errs.push(`A ${key}: sheet ${s + 1}'s content box leaves ${lead} / ${trail} in`)
|
||||
})
|
||||
// B. the visible parts tile the finished poster (1/8 in samples)
|
||||
const hits = new Array(Math.round(total * 8)).fill(0)
|
||||
P.sheets.forEach((sh, s) => {
|
||||
const off = V ? sh.at.y : sh.at.x, from = off + (sh.band ? P.joinIn : 0), to = off + along
|
||||
for (let k = 0; k < hits.length; k++) { const t = (k + 0.5) / 8; if (t >= from && t < to) hits[k]++ }
|
||||
})
|
||||
const bad = hits.findIndex((h) => h !== 1)
|
||||
if (bad >= 0) errs.push(`B ${key}: at ${(bad + 0.5) / 8} in the poster is shown by ${hits[bad]} sheets, not 1`)
|
||||
// C. slices
|
||||
const sl = P.sheets.map((_, s) => M.sliceFor(P, s))
|
||||
for (let s = 1; s < sl.length; s++) {
|
||||
const endPrev = V ? sl[s - 1].y + sl[s - 1].h : sl[s - 1].x + sl[s - 1].w, start = V ? sl[s].y : sl[s].x
|
||||
const want = joinFill === 'bleed' ? endPrev - z.joinIn : endPrev
|
||||
if (!eq(start, want)) errs.push(`C ${key} ${joinFill}: slice ${s + 1} starts at ${start} in, want ${want}`)
|
||||
}
|
||||
const last = sl[sl.length - 1]
|
||||
if (!eq(V ? last.y + last.h : last.x + last.w, total) || !eq(V ? sl[0].y : sl[0].x, 0)) errs.push(`C ${key} ${joinFill}: the slices do not span the poster`)
|
||||
// D. marks
|
||||
P.sheets.forEach((sh, s) => {
|
||||
const pr = M.panelChromeHtml(P, s, {}), sc = M.panelChromeHtml(P, s, { screen: true })
|
||||
const solid = /class="glue[ "]/.test(pr.before), guide = /class="glue-guide/.test(pr.before)
|
||||
if (sh.band && joinFill === 'glue' && !solid) errs.push(`D ${key}: a glue join does not print its red band`)
|
||||
if (sh.band && joinFill === 'bleed' && (solid || !guide)) errs.push(`D ${key}: a bleed join prints ${solid ? 'a solid band' : 'no guide'}`)
|
||||
if (sh.band && !/class="glue[ "]/.test(sc.before)) errs.push(`D ${key}: the screen preview does not draw the red band`)
|
||||
if (!sh.band && (pr.before || sc.before)) errs.push(`D ${key}: sheet 1 draws a join band`)
|
||||
if (/class="trim/.test(pr.after) !== sh.trimmed) errs.push(`D ${key}: sheet ${s + 1}'s trim line is wrong`)
|
||||
if (!pr.after.includes(`>${s + 1} / ${z.count}<`)) errs.push(`D ${key}: sheet ${s + 1} has no "${s + 1} / ${z.count}" tag`)
|
||||
})
|
||||
checked++
|
||||
}
|
||||
}
|
||||
return { errs, checked }
|
||||
}
|
||||
|
||||
const fails = []
|
||||
const r = run(real)
|
||||
fails.push(...r.errs)
|
||||
if (r.checked < 6) fails.push(`only ${r.checked} of 6 size × fill cases ran`)
|
||||
// E. the Bless consumer
|
||||
const plan = bless.planPoster('5x8ft', 2107)
|
||||
const P = real.planPanels({ sheetW: 60, sheetH: 36, count: 3, direction: 'vertical', joinIn: 6, borderIn: 6, joinFill: 'glue' })
|
||||
plan.bands.forEach((bd, s) => {
|
||||
const c = P.sheets[s].content
|
||||
if (!eq(bd.y0 - (s === 0 ? plan.bannerH : 0), c.y0) || !eq(bd.y1 + (s === 2 ? plan.footerH + (plan.gapH || 0) : 0), c.y1)) fails.push(`E: Bless 5x8ft sheet ${s + 1}'s band is not the panels' content box`)
|
||||
})
|
||||
if (!eq(plan.posterW, P.finished.w) || !eq(plan.posterH, P.finished.h)) fails.push('E: the Bless poster\'s finished size is not the panels\'')
|
||||
if (!/from '[./]+library\/components\/poster\/panelSheets\.js'/.test(blessSrc)) fails.push('E: blessPoster.js does not import panelSheets.js')
|
||||
if (/class="glue"/.test(blessSrc) || /\.glue\{/.test(blessSrc)) fails.push('E: blessPoster.js carries its own glue band (a private copy to drift)')
|
||||
if (!/class="glue"/.test(bless.buildBlessPosterHtml(Array.from({ length: 50 }, () => ({ name: 'x' })), '5x8ft', {}))) fails.push('E: the Bless 5x8ft print has no glue band')
|
||||
|
||||
// Two-sided: each arm against a planted defect.
|
||||
const planted = {
|
||||
'A/B (the joins not subtracted)': { ...real, planPanels: (o) => { const p = real.planPanels(o); const V = p.direction === 'vertical'; return { ...p, finished: V ? { w: p.sheetW, h: p.count * p.sheetH } : { w: p.count * p.sheetW, h: p.sheetH } } } },
|
||||
'C (a glue slice keeps the band)': { ...real, sliceFor: (p, s) => real.sliceFor({ ...p, joinFill: 'bleed' }, s) },
|
||||
'D (a bleed join prints a solid band)': { ...real, panelChromeHtml: (p, s, o = {}) => real.panelChromeHtml(p, s, { ...o, screen: true }) },
|
||||
'D (no trim line)': { ...real, panelChromeHtml: (p, s, o) => { const h = real.panelChromeHtml(p, s, o); return { ...h, after: h.after.replace(/<div class="trim[^>]*><\/div>/, '') } } },
|
||||
}
|
||||
for (const [name, M] of Object.entries(planted)) {
|
||||
const want = name[0]
|
||||
if (!run(M).errs.some((e) => e.startsWith(want) || (want === 'A' && e.startsWith('B')))) fails.push(`SELF-TEST: the planted defect "${name}" was not caught`)
|
||||
}
|
||||
|
||||
if (fails.length) { console.log('PANEL-SHEETS: FAIL'); for (const f of fails) console.log(' - ' + f); process.exitCode = 1 }
|
||||
else console.log(`PANEL-SHEETS: PASS — ${r.checked} size × fill cases (60x96 stacked, 66x66 side by side, one sheet; glue and bleed): the sheets tile the finished poster, the slices tile or repeat exactly the join, the marks follow the fill, and the Bless 5x8ft takes its bands and marks from panelSheets.js; ${Object.keys(planted).length} planted defects caught.`)
|
||||
352
internal/check-poster-wall-host-seams.mjs
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-poster-wall-host-seams.mjs: PosterWall's two host seams, headerAction and previewSrc, do what they
|
||||
* say, and with neither set every wall behaves as before.
|
||||
*
|
||||
* WHY (headerAction). PosterWall's card header was hard-wired to "Show on map". A wall with no map beside it (the
|
||||
* Poster Design gallery) needs the same card with a different action, so the header became a seam:
|
||||
* headerAction null (the default) keeps the "Show on map" TOGGLE; { labelKey, ariaKey } makes it a
|
||||
* plain button that emits 'header-action'. Every existing wall passes nothing, so the default path is
|
||||
* the one that must not move, and the new path must not leak the old one's side effects (a window
|
||||
* event a map would act on, aria-pressed, the is-last highlight).
|
||||
*
|
||||
* WHY (previewSrc). The card's thumbnail was always the stored previews/<id>.webp. A gallery of templates
|
||||
* drawn from the library has no stored file and must show a library change, so the thumbnail can come
|
||||
* from the host. A gallery renders many cards, so the seam is LAZY (only cards in view), ONE render at a
|
||||
* time, cached per page, and bounded: no answer, a throw or a timeout falls back to the wall's
|
||||
* "preview unavailable" placeholder, never a blank or an endless wait.
|
||||
*
|
||||
* HOW. No browser (fleet policy). PosterWall has no <style> block, so it is compiled here as a CLIENT
|
||||
* component (compiler-sfc, inline template) and loaded through Vite with the repo's @map alias. It is
|
||||
* mounted on a recording renderer: every element keeps its tag, props and children, so the gate reads
|
||||
* the rendered header buttons and CALLS their real onClick. Two arms, one card each:
|
||||
* A (default) one .tape-h, aria-pressed="false"; a click dispatches ONE window event named by
|
||||
* eventName with listId = the item id, sets aria-pressed="true" and is-last, and
|
||||
* emits nothing; a second click dispatches listId null (the toggle).
|
||||
* B (headerAction) one .tape-h, NO aria-pressed, label = "7 UPG ·" + t(labelKey), aria-label = t(ariaKey);
|
||||
* a click emits 'header-action' with the item, dispatches NO window event, sets no is-last.
|
||||
* C (headerAction, an item with no count) the sticker reads t(labelKey) alone: no "UPG", no "·".
|
||||
* D (no previewSrc) the card is an <img class="img"> on previews/<id>.webp; no pending placeholder, and the
|
||||
* wall creates no IntersectionObserver.
|
||||
* E (previewSrc) 3 cards, all pending, 0 renders before any is in view (lazy), observed with the wall's
|
||||
* own scroller as the root (it clips its cards). Two come into view: exactly
|
||||
* one render runs; when it answers a URL that card becomes an <img> with that src and the
|
||||
* second render starts; an answer of null gives the "preview unavailable" placeholder. The
|
||||
* card never in view is never rendered.
|
||||
* F (previewSrc that never answers) after the timeout the card shows the placeholder.
|
||||
* G (cache) a remount with the same instanceId shows the cached URL with no second render.
|
||||
* H (no previewSrc, the stored webp fails) the first error retries the tree's own copy
|
||||
* (<tree>/assets/previews/<id>.webp) and only the second shows the placeholder.
|
||||
* I (previewSrc, the generated image fails) one error shows the placeholder: there is no second base.
|
||||
* embedBase.js is served to the gate as if from a BUILT tree (<tree>/<bundle>/<bundle>.js), because
|
||||
* PosterWall's second base is derived from that URL; from source it is empty and H could not fail.
|
||||
* A vacuous run (no .tape-h found, or no items rendered) FAILS.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const WALL = join(ROOT, 'library/components/poster/PosterWall.vue')
|
||||
const failures = []
|
||||
const fail = (m) => failures.push(m)
|
||||
|
||||
// ── the thinnest page PosterWall's setup touches ───────────────────────────────────────────────
|
||||
const dispatched = []
|
||||
const observers = []
|
||||
{
|
||||
const noop = () => {}
|
||||
const el = () => ({ style: {}, dataset: {}, setAttribute: noop, getAttribute: () => null, removeAttribute: noop,
|
||||
appendChild: (c) => c, removeChild: noop, addEventListener: noop, removeEventListener: noop,
|
||||
querySelector: () => null, querySelectorAll: () => [], getRootNode() { return this },
|
||||
classList: { add: noop, remove: noop, toggle: noop, contains: () => false } })
|
||||
const html = el(); html.lang = 'en'
|
||||
Object.assign(globalThis, {
|
||||
window: globalThis, self: globalThis,
|
||||
document: Object.assign(el(), { documentElement: html, head: el(), body: el(), createElement: el, createTextNode: el,
|
||||
currentScript: null, readyState: 'complete', cookie: '', getElementById: () => null }),
|
||||
location: new URL(`https://gate.invalid/${TREE_NAME}/x/x/index.html?lang=en`),
|
||||
localStorage: { getItem: () => null, setItem: noop, removeItem: noop },
|
||||
sessionStorage: { getItem: () => null, setItem: noop, removeItem: noop },
|
||||
matchMedia: () => ({ matches: false, addEventListener: noop, removeEventListener: noop, addListener: noop, removeListener: noop }),
|
||||
addEventListener: noop, removeEventListener: noop,
|
||||
dispatchEvent: (e) => { dispatched.push(e); return true },
|
||||
requestAnimationFrame: (f) => setTimeout(f, 0), cancelAnimationFrame: clearTimeout,
|
||||
ResizeObserver: class { observe() {} unobserve() {} disconnect() {} },
|
||||
// A RECORDING observer, so the gate can bring a card into view on demand.
|
||||
IntersectionObserver: class { constructor(cb, opts) { this.cb = cb; this.opts = opts || {}; this.els = new Set(); observers.push(this) }
|
||||
observe(e) { this.els.add(e) } unobserve(e) { this.els.delete(e) } disconnect() { this.els.clear() } },
|
||||
MutationObserver: class { observe() {} disconnect() {} },
|
||||
HTMLElement: class {}, ShadowRoot: globalThis.ShadowRoot || class ShadowRoot {},
|
||||
CustomEvent: class { constructor(type, o) { this.type = type; this.detail = o?.detail } },
|
||||
})
|
||||
if (!globalThis.navigator?.language) globalThis.navigator = { language: 'en', languages: ['en'], userAgent: 'gate' }
|
||||
}
|
||||
|
||||
// ── PosterWall as a CLIENT component, loaded through Vite for the alias ────────────────────────
|
||||
const CLIENT_ID = '\0poster-wall-client.js'
|
||||
// PosterWall's fallback base comes from the bundle's own script URL (embedBase.js bundleTreeRoot), which
|
||||
// from source is not a built bundle URL, so the fallback would be empty and arm H vacuous.
|
||||
const GATE_TREE = 'https://gate.invalid/tree'
|
||||
const builtScriptUrl = {
|
||||
name: 'gate-built-script-url',
|
||||
transform(code, id) {
|
||||
if (!id.endsWith('/library/utils/embedBase.js')) return null
|
||||
if (!/const SCRIPT_URL = import\.meta\.url/.test(code)) throw new Error('embedBase.js no longer reads SCRIPT_URL from import.meta.url; update arm H')
|
||||
return code.replace(/const SCRIPT_URL = import\.meta\.url/, `const SCRIPT_URL = ${JSON.stringify(GATE_TREE + '/poster-design/poster-design.js')}`)
|
||||
},
|
||||
}
|
||||
const clientCompile = {
|
||||
name: 'gate-poster-wall-client',
|
||||
resolveId: (id) => (id === 'virtual:poster-wall-client' ? CLIENT_ID : null),
|
||||
async load(id) {
|
||||
if (id !== CLIENT_ID) return null
|
||||
const sfc = await import('@vue/compiler-sfc')
|
||||
const { descriptor, errors } = sfc.parse(readFileSync(WALL, 'utf8'), { filename: WALL })
|
||||
if (errors.length) throw new Error(`PosterWall.vue did not parse: ${errors[0].message}`)
|
||||
const out = sfc.compileScript(descriptor, { id: 'pw-gate', inlineTemplate: true })
|
||||
// Relative imports are relative to the .vue, not to this virtual id.
|
||||
return out.content.replace(/from\s+(['"])\.\.\/\.\.\//g, `from $1${join(ROOT, 'library')}/`)
|
||||
},
|
||||
}
|
||||
|
||||
const { createServer } = await import('vite')
|
||||
const server = await createServer({
|
||||
root: ROOT, configFile: false, logLevel: 'error', appType: 'custom',
|
||||
server: { middlewareMode: true, hmr: false, ws: false, watch: null },
|
||||
plugins: [clientCompile, builtScriptUrl],
|
||||
resolve: { alias: { '@map': join(ROOT, 'library') } },
|
||||
define: { __DOXA_TREE_NAME__: JSON.stringify(TREE_NAME) },
|
||||
})
|
||||
|
||||
// ── a renderer that records what it draws ──────────────────────────────────────────────────────
|
||||
// Recorded elements also answer the few DOM calls PosterWall's hooks make (v-show writes style.display,
|
||||
// the row fitter queries the wall, useShadowStyles finds its root node).
|
||||
const noopFn = () => {}
|
||||
function node(tag) {
|
||||
return { tag, props: {}, children: [], parent: null, text: '', style: { setProperty: noopFn, getPropertyValue: () => '' },
|
||||
dataset: {}, classList: { add: noopFn, remove: noopFn, toggle: noopFn, contains: () => false },
|
||||
querySelector: () => null, querySelectorAll: () => [], addEventListener: noopFn, removeEventListener: noopFn,
|
||||
setAttribute: noopFn, getAttribute: () => null, removeAttribute: noopFn, appendChild: (c) => c,
|
||||
getBoundingClientRect: () => ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0 }),
|
||||
getRootNode() { return { host: null, adoptedStyleSheets: [], appendChild: (c) => c, querySelector: () => null } } }
|
||||
}
|
||||
function insert(child, parent, anchor) {
|
||||
if (child.parent) child.parent.children.splice(child.parent.children.indexOf(child), 1)
|
||||
child.parent = parent
|
||||
const i = anchor ? parent.children.indexOf(anchor) : -1
|
||||
if (i >= 0) parent.children.splice(i, 0, child); else parent.children.push(child)
|
||||
}
|
||||
const walk = (n, f) => { f(n); for (const c of n.children) walk(c, f) }
|
||||
const textOf = (n) => { let s = ''; walk(n, (x) => { if (x.tag === '#text') s += x.text }); return s }
|
||||
const hasClass = (n, c) => String(n.props.class || '').split(/\s+/).includes(c)
|
||||
const classStr = (v) => (typeof v === 'string' ? v : Array.isArray(v) ? v.map(classStr).join(' ')
|
||||
: v && typeof v === 'object' ? Object.keys(v).filter((k) => v[k]).join(' ') : '')
|
||||
|
||||
let buttonsSeen = 0
|
||||
try {
|
||||
const rc = await import('@vue/runtime-core')
|
||||
const { createApp } = rc.createRenderer({
|
||||
insert, remove: (c) => { if (c.parent) c.parent.children.splice(c.parent.children.indexOf(c), 1); c.parent = null },
|
||||
createElement: (tag) => node(tag), createText: (t) => Object.assign(node('#text'), { text: t }),
|
||||
createComment: () => node('#comment'), setText: (n, t) => { n.text = t },
|
||||
setElementText: (n, t) => { n.children = [Object.assign(node('#text'), { text: t, parent: n })] },
|
||||
parentNode: (n) => n.parent, nextSibling: (n) => { const p = n.parent; return p ? p.children[p.children.indexOf(n) + 1] || null : null },
|
||||
patchProp: (el, key, _prev, next) => { el.props[key] = key === 'class' ? classStr(next) : next },
|
||||
})
|
||||
const PosterWall = (await server.ssrLoadModule('virtual:poster-wall-client')).default
|
||||
const item = { id: 'gate-item', label: 'Gate Item', count: 7 }
|
||||
|
||||
async function mountWall(extra, items = [item]) {
|
||||
const emitted = []
|
||||
const root = node('root')
|
||||
const app = createApp(PosterWall, { items, eventName: 'gate:show', instanceId: 'gate', ...extra,
|
||||
'onHeader-action': (i) => emitted.push(i) })
|
||||
app.config.errorHandler = (e) => fail(`mount error: ${e?.message || e}`)
|
||||
app.mount(root)
|
||||
await rc.nextTick()
|
||||
const tapes = []; walk(root, (n) => { if (n.tag === 'button' && hasClass(n, 'tape-h')) tapes.push(n) })
|
||||
const cards = []; walk(root, (n) => { if (n.tag === 'div' && hasClass(n, 'card')) cards.push(n) })
|
||||
buttonsSeen += tapes.length
|
||||
return { root, tapes, cards, emitted, app }
|
||||
}
|
||||
|
||||
// ── A: the default is the toggle it always was ─────────────────────────────────────────────
|
||||
{
|
||||
dispatched.length = 0
|
||||
const w = await mountWall({})
|
||||
if (w.cards.length !== 1) fail(`A: expected 1 card, rendered ${w.cards.length}`)
|
||||
if (w.tapes.length !== 1) fail(`A: expected 1 header sticker, found ${w.tapes.length}`)
|
||||
const b = w.tapes[0]
|
||||
if (b) {
|
||||
if (b.props['aria-pressed'] !== 'false') fail(`A: aria-pressed should start "false", got ${JSON.stringify(b.props['aria-pressed'])}`)
|
||||
if (typeof b.props.onClick !== 'function') fail('A: the sticker has no click handler')
|
||||
else {
|
||||
b.props.onClick(); await rc.nextTick()
|
||||
const ev = dispatched.filter((e) => e.type === 'gate:show')
|
||||
if (ev.length !== 1 || ev[0].detail?.listId !== item.id) fail(`A: first click should dispatch gate:show with listId ${item.id}, got ${JSON.stringify(ev.map((e) => e.detail))}`)
|
||||
if (b.props['aria-pressed'] !== 'true') fail('A: aria-pressed should be "true" after the first click')
|
||||
if (!hasClass(w.cards[0], 'is-last')) fail('A: the card should carry is-last after the first click')
|
||||
b.props.onClick(); await rc.nextTick()
|
||||
const ev2 = dispatched.filter((e) => e.type === 'gate:show')
|
||||
if (ev2.length !== 2 || ev2[1].detail?.listId !== null) fail('A: second click should dispatch listId null (the toggle)')
|
||||
}
|
||||
if (w.emitted.length) fail(`A: the default must emit nothing, emitted ${w.emitted.length}`)
|
||||
}
|
||||
w.app.unmount()
|
||||
}
|
||||
|
||||
// ── B: headerAction swaps the action and drops the toggle's side effects ────────────────────
|
||||
{
|
||||
dispatched.length = 0
|
||||
const w = await mountWall({ headerAction: { labelKey: 'gate.fullDetails', ariaKey: 'gate.fullDetailsAria' } })
|
||||
if (w.tapes.length !== 1) fail(`B: expected 1 header sticker, found ${w.tapes.length}`)
|
||||
const b = w.tapes[0]
|
||||
if (b) {
|
||||
if ('aria-pressed' in b.props && b.props['aria-pressed'] != null) fail(`B: a plain button must not carry aria-pressed, got ${JSON.stringify(b.props['aria-pressed'])}`)
|
||||
const label = textOf(b)
|
||||
// No i18n is installed, so t() falls back to humanizing the key: the label must come from
|
||||
// labelKey, and must not be the map wording.
|
||||
if (!/full\s*details/i.test(label)) fail(`B: label should come from labelKey, got ${JSON.stringify(label)}`)
|
||||
if (/on\s*map/i.test(label)) fail(`B: label still says the map wording: ${JSON.stringify(label)}`)
|
||||
if (!/7\s*UPG/.test(label)) fail(`B: an item WITH a count keeps its "7 UPG ·" prefix, got ${JSON.stringify(label)}`)
|
||||
if (!/aria/i.test(String(b.props['aria-label']))) fail(`B: aria-label should come from ariaKey, got ${JSON.stringify(b.props['aria-label'])}`)
|
||||
if (typeof b.props.onClick !== 'function') fail('B: the sticker has no click handler')
|
||||
else {
|
||||
b.props.onClick(); await rc.nextTick()
|
||||
if (w.emitted.length !== 1 || w.emitted[0]?.id !== item.id) fail(`B: a click should emit header-action with the item once, emitted ${JSON.stringify(w.emitted)}`)
|
||||
if (dispatched.some((e) => e.type === 'gate:show')) fail('B: a click must dispatch NO show-on-map window event')
|
||||
if (hasClass(w.cards[0], 'is-last')) fail('B: the card must not take the is-last highlight')
|
||||
}
|
||||
}
|
||||
w.app.unmount()
|
||||
}
|
||||
|
||||
// ── C: a template card has no count, so its sticker is the label alone ─────────────────────
|
||||
{
|
||||
const w = await mountWall({ headerAction: { labelKey: 'gate.fullDetails' } }, [{ id: 'gate-template', label: 'Gate Template' }])
|
||||
const label = w.tapes[0] ? textOf(w.tapes[0]) : ''
|
||||
if (w.tapes.length !== 1) fail(`C: expected 1 header sticker, found ${w.tapes.length}`)
|
||||
else if (/UPG|·|undefined/.test(label) || !/full\s*details/i.test(label)) fail(`C: a countless item should read the label alone, got ${JSON.stringify(label)}`)
|
||||
w.app.unmount()
|
||||
}
|
||||
|
||||
// ── the previewSrc seam ───────────────────────────────────────────────────────────────────────
|
||||
const settle = async () => { for (let i = 0; i < 6; i++) { await new Promise((r) => setTimeout(r, 0)); await rc.nextTick() } }
|
||||
const imgs = (root) => { const out = []; walk(root, (n) => { if (n.tag === 'img' && hasClass(n, 'img')) out.push(n) }); return out }
|
||||
const pending = (root) => { const out = []; walk(root, (n) => { if (n.tag === 'div' && hasClass(n, 'ph') && hasClass(n, 'is-pending')) out.push(n) }); return out }
|
||||
const phFor = (root, label) => { let hit = null; walk(root, (n) => { if (!hit && n.tag === 'div' && hasClass(n, 'ph') && textOf(n).includes(label)) hit = n }); return hit }
|
||||
const inView = (el) => { for (const io of observers) if (io.els.has(el)) io.cb([{ target: el, isIntersecting: true }]) }
|
||||
|
||||
// D: no previewSrc, the stored file exactly as before
|
||||
{
|
||||
const before = observers.length
|
||||
const w = await mountWall({})
|
||||
const im = imgs(w.root)
|
||||
if (im.length !== 1 || !String(im[0].props.src).endsWith('/previews/gate-item.webp')) fail(`D: expected one img on previews/gate-item.webp, got ${JSON.stringify(im.map((x) => x.props.src))}`)
|
||||
if (pending(w.root).length) fail('D: a wall without previewSrc must show no pending placeholder')
|
||||
if (observers.length !== before) fail('D: a wall without previewSrc must create no IntersectionObserver')
|
||||
w.app.unmount()
|
||||
}
|
||||
|
||||
// E + G: lazy, one at a time, fallback on null, cached
|
||||
const three = [{ id: 'p1', label: 'Pone', count: 3 }, { id: 'p2', label: 'Ptwo', count: 2 }, { id: 'p3', label: 'Pthree', count: 1 }]
|
||||
const calls = []; let inFlight = 0; let maxInFlight = 0; const answer = {}
|
||||
const previewSrc = (it) => { calls.push(it.id); inFlight++; maxInFlight = Math.max(maxInFlight, inFlight)
|
||||
return new Promise((res) => { answer[it.id] = (v) => { inFlight--; res(v) } }) }
|
||||
{
|
||||
const w = await mountWall({ previewSrc, instanceId: 'gate-e' }, three)
|
||||
await settle()
|
||||
if (pending(w.root).length !== 3) fail(`E: expected 3 pending cards before any is in view, got ${pending(w.root).length}`)
|
||||
if (imgs(w.root).length) fail('E: no card may show an img before its preview arrives')
|
||||
if (calls.length) fail(`E: LAZY broken, ${calls.length} render(s) before any card was in view`)
|
||||
// A pane wall scrolls inside .wall, so the observer must watch THAT scroller or its margin never looks ahead.
|
||||
const io = observers[observers.length - 1]
|
||||
if (!io || !io.opts.root || io.opts.root.tag !== 'div' || !hasClass(io.opts.root, 'wall')) fail(`E: the observer's root should be the wall's own scroller (.wall), got ${io?.opts.root ? io.opts.root.tag + '.' + io.opts.root.props.class : String(io?.opts.root)}`)
|
||||
inView(phFor(w.root, 'Pone')); inView(phFor(w.root, 'Ptwo')); await settle()
|
||||
if (calls.length !== 1) fail(`E: ONE AT A TIME broken, ${calls.length} renders running with two cards in view`)
|
||||
answer.p1?.('blob:gate-one'); await settle()
|
||||
const im = imgs(w.root)
|
||||
if (im.length !== 1 || im[0].props.src !== 'blob:gate-one') fail(`E: the answered card should be an img on blob:gate-one, got ${JSON.stringify(im.map((x) => x.props.src))}`)
|
||||
if (calls.length !== 2 || calls[1] !== 'p2') fail(`E: the second render should start after the first answered, calls ${JSON.stringify(calls)}`)
|
||||
answer.p2?.(null); await settle()
|
||||
const ph2 = phFor(w.root, 'Ptwo')
|
||||
if (!ph2 || hasClass(ph2, 'is-pending') || !/unavailable/i.test(textOf(ph2))) fail(`E: a null answer should show the "preview unavailable" placeholder, got ${JSON.stringify(ph2 && textOf(ph2))}`)
|
||||
if (calls.includes('p3')) fail('E: a card never in view was rendered')
|
||||
if (maxInFlight !== 1) fail(`E: ${maxInFlight} renders overlapped`)
|
||||
w.app.unmount()
|
||||
}
|
||||
{
|
||||
const n = calls.length
|
||||
const w = await mountWall({ previewSrc, instanceId: 'gate-e' }, three)
|
||||
await settle()
|
||||
const p1 = phFor(w.root, 'Pone'); if (p1) inView(p1)
|
||||
await settle()
|
||||
if (calls.length !== n) fail(`G: CACHE broken, a remount rendered again (${calls.slice(n).join(',')})`)
|
||||
const im = imgs(w.root)
|
||||
if (!im.some((x) => x.props.src === 'blob:gate-one')) fail('G: the remount should show the cached blob:gate-one')
|
||||
w.app.unmount()
|
||||
}
|
||||
|
||||
// F: a render that never answers is bounded by the timeout
|
||||
{
|
||||
const realSetTimeout = globalThis.setTimeout
|
||||
globalThis.setTimeout = (f, ms, ...a) => realSetTimeout(f, ms >= 10000 ? 1 : ms, ...a)
|
||||
try {
|
||||
const w = await mountWall({ previewSrc: () => new Promise(() => {}), instanceId: 'gate-f' }, [{ id: 'f1', label: 'Fone', count: 1 }])
|
||||
await settle()
|
||||
inView(phFor(w.root, 'Fone'))
|
||||
await settle(); await new Promise((r) => realSetTimeout(r, 20)); await settle()
|
||||
const ph = phFor(w.root, 'Fone')
|
||||
if (!ph || hasClass(ph, 'is-pending') || !/unavailable/i.test(textOf(ph))) fail(`F: a render that never answers should end on the placeholder, got ${JSON.stringify(ph && textOf(ph))}`)
|
||||
w.app.unmount()
|
||||
} finally { globalThis.setTimeout = realSetTimeout }
|
||||
}
|
||||
|
||||
// H: a default wall's broken stored webp tries the tree's copy before giving up
|
||||
{
|
||||
const w = await mountWall({})
|
||||
const first = imgs(w.root)[0]
|
||||
const firstSrc = first?.props.src
|
||||
if (!first || typeof first.props.onError !== 'function') fail('H: the default card has no img error handler')
|
||||
else {
|
||||
first.props.onError(); await settle()
|
||||
const second = imgs(w.root)[0]
|
||||
const want = `${GATE_TREE}/assets/previews/gate-item.webp`
|
||||
if (!second || second.props.src !== want) fail(`H: the first error should retry ${want}, got ${JSON.stringify(second?.props.src)} (was ${JSON.stringify(firstSrc)})`)
|
||||
else {
|
||||
second.props.onError(); await settle()
|
||||
const ph = phFor(w.root, 'Gate Item')
|
||||
if (imgs(w.root).length || !ph || !/unavailable/i.test(textOf(ph))) fail('H: the second error should show the "preview unavailable" placeholder')
|
||||
}
|
||||
}
|
||||
w.app.unmount()
|
||||
}
|
||||
|
||||
// I: a generated preview that fails to load has no second base
|
||||
{
|
||||
const w = await mountWall({ previewSrc: async () => 'blob:gate-broken', instanceId: 'gate-i' }, [{ id: 'i1', label: 'Ione', count: 1 }])
|
||||
await settle(); inView(phFor(w.root, 'Ione')); await settle()
|
||||
const im = imgs(w.root)[0]
|
||||
if (!im || im.props.src !== 'blob:gate-broken') fail(`I: the generated URL should be on the img, got ${JSON.stringify(im?.props.src)}`)
|
||||
else {
|
||||
im.props.onError(); await settle()
|
||||
const ph = phFor(w.root, 'Ione')
|
||||
if (imgs(w.root).length || !ph || !/unavailable/i.test(textOf(ph))) fail('I: one error on a generated preview should show the placeholder, not retry another base')
|
||||
}
|
||||
w.app.unmount()
|
||||
}
|
||||
} catch (e) {
|
||||
fail(`threw: ${e?.stack || e}`)
|
||||
} finally {
|
||||
await server.close()
|
||||
}
|
||||
|
||||
if (!buttonsSeen) fail('vacuous: no header sticker rendered in either arm')
|
||||
if (failures.length) {
|
||||
console.error(`POSTER-WALL-HOST-SEAMS: FAIL (${failures.length})\n - ${failures.join('\n - ')}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`POSTER-WALL-HOST-SEAMS: PASS (9 arms, ${buttonsSeen} stickers rendered; defaults unchanged: the Show-on-map toggle and the stored webp; headerAction emits, previewSrc is lazy, one at a time, cached and bounded)`)
|
||||
}
|
||||
|
|
@ -63,8 +63,19 @@ const HAND_VOCAB = {
|
|||
'Project 700',
|
||||
'How this list was found',
|
||||
],
|
||||
// The phrases that NAME a series. Derived vocabulary drops any sentence two series share, so a verbatim
|
||||
// copy of one series' framing into another would go unseen without these (the copy becomes "shared").
|
||||
country: [
|
||||
'PRAY FOR THIS COUNTRY',
|
||||
'Country Prayer Map',
|
||||
'one sheet for each country',
|
||||
'which country God has put',
|
||||
],
|
||||
indiaState: [
|
||||
'PRAY FOR THIS STATE',
|
||||
'State Prayer Map',
|
||||
'one sheet for each state',
|
||||
'which state God has put',
|
||||
],
|
||||
}
|
||||
|
||||
|
|
@ -100,13 +111,13 @@ function deriveVocab(series) {
|
|||
...strings(e.poster || {}),
|
||||
].map((s) => s.trim()).filter((s) => s.length >= 12))
|
||||
}
|
||||
// Drop anything two series share — it cannot identify either of them.
|
||||
for (const a of ids) {
|
||||
for (const b of ids) {
|
||||
if (a === b) continue
|
||||
for (const s of own[b]) own[a].delete(s)
|
||||
}
|
||||
}
|
||||
// Drop anything two series share — it cannot identify either of them. The shared set is found on the
|
||||
// ORIGINAL sets first: deleting while still comparing pairs removed a shared string from the earlier
|
||||
// series only, so the later one kept it as a "fingerprint" and the earlier one failed for carrying it.
|
||||
// Invisible with two series; a third (indiaState, sharing country's networks) exposed it.
|
||||
const shared = new Set()
|
||||
for (const a of ids) for (const b of ids) if (a !== b) for (const s of own[a]) if (own[b].has(s)) shared.add(s)
|
||||
for (const id of ids) for (const s of shared) own[id].delete(s)
|
||||
const out = {}
|
||||
for (const id of ids) out[id] = [...new Set([...(HAND_VOCAB[id] || []), ...own[id]])]
|
||||
return out
|
||||
|
|
@ -120,8 +131,10 @@ function* strings(v) {
|
|||
}
|
||||
|
||||
/** The text a series OWNS: its overrides and its poster furniture. Not its prose documentation. */
|
||||
// The poster header block too: that is where the country poster once printed the UPG-100 side box, and a scan
|
||||
// of strings + networks alone could never see a series' framing copied into another's poster text.
|
||||
function seriesText(entry) {
|
||||
return [...strings(entry.strings || {}), ...strings(entry.networks || [])]
|
||||
return [...strings(entry.strings || {}), ...strings(entry.networks || []), ...strings(entry.poster || {})]
|
||||
}
|
||||
|
||||
function checkDeclarations(series) {
|
||||
|
|
@ -208,6 +221,9 @@ function selfTest() {
|
|||
['upg100 loses its funnel', (s) => { s.upg100.funnel = false }],
|
||||
['a series loses a networks block', (s) => { s.country.networks.pop() }],
|
||||
['dataDir points nowhere', (s) => { s.country.dataDir = 'no/such/folder' }],
|
||||
['indiaState keeps the country side box', (s) => { s.indiaState.poster.sideBoxHeading = 'What is a Country Prayer Map?' }],
|
||||
['country keeps the state network line', (s) => { s.country.networks[0][1] = s.indiaState.networks[0][1] }],
|
||||
['indiaState keeps PRAY FOR THIS COUNTRY', (s) => { s.indiaState.strings.map_page_kicker = 'PRAY FOR THIS COUNTRY' }],
|
||||
]
|
||||
let pass = 0
|
||||
for (const [name, mutate] of cases) {
|
||||
|
|
@ -224,6 +240,19 @@ function selfTest() {
|
|||
fails.length = 0
|
||||
if (pass !== cases.length) fail(`self-test: caught ${pass}/${cases.length} known-bad inputs`)
|
||||
else ok(`self-test: caught all ${cases.length} known-bad inputs`)
|
||||
// Shared wording is nobody's fingerprint, in EITHER order: three series where the first and last share a
|
||||
// sentence must neither flag it nor lose their own phrases.
|
||||
const SHARED = 'A sentence two series both print, word for word.'
|
||||
const tri = {
|
||||
a: { strings: { k: 'Only series a says this phrase.' }, networks: [['h', SHARED]] },
|
||||
b: { strings: { k: 'Only series b says this phrase.' }, networks: [] },
|
||||
c: { strings: { k: 'Only series c says this phrase.' }, networks: [['h', SHARED]] },
|
||||
}
|
||||
const v = deriveVocab(tri)
|
||||
const leaked = Object.entries(v).filter(([, p]) => p.includes(SHARED)).map(([id]) => id)
|
||||
const lost = ['a', 'b', 'c'].filter((id) => !v[id].includes(`Only series ${id} says this phrase.`))
|
||||
if (leaked.length || lost.length) fail(`self-test: shared wording kept as vocabulary of [${leaked}] / own phrases lost for [${lost}]`)
|
||||
else ok('self-test: shared wording is dropped from every series, whatever their order')
|
||||
}
|
||||
|
||||
// ── run ──────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -167,13 +167,18 @@ const run = (rule, src) => rule.test ? rule.test(src) : [...src.matchAll(rule.re
|
|||
const lineOf = (src, idx) => src.slice(0, idx).split('\n').length
|
||||
|
||||
// ── self-test: every rule must see a planted violation ─────────────────────────────────────────
|
||||
// The TOK-LITERAL sample is ASSEMBLED HERE, never written as one literal: a committed string shaped like a
|
||||
// real Mapbox token is itself what that rule forbids, and reads as a leaked credential to any secret scan.
|
||||
// It is plainly fake ('example, not a token'), yet shaped like one: pk. + base64url JSON + . + a tail, which
|
||||
// is all the rule's regex looks for, so the self-test still proves the rule fires.
|
||||
const FAKE_TOKEN = 'pk.' + Buffer.from('{"u":"example","note":"not-a-token"}').toString('base64url') + '.' + 'x'.repeat(22)
|
||||
const PLANTED = `
|
||||
map.setStyle(isDark ? 'mapbox://styles/mapbox/dark-v11' : 'x')
|
||||
const m2 = new gl.Map({ container: el, style: 'mapbox://styles/mapbox/light-v11' })
|
||||
const p = new mapboxgl.Popup()
|
||||
useMapInstance({ container: el, provider: 'mapbox' })
|
||||
const tk = window.DOXA_TOKEN
|
||||
const t = 'pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJjbGFudGVkIn0'
|
||||
const t = '${FAKE_TOKEN}'
|
||||
n.toLocaleString('en-US')
|
||||
const r = await fetch(base + '/api/people-groups/list?fields=slug')
|
||||
const dsm = new DataSourceManager({ locale: 'en' })
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ function readBuildConfig() {
|
|||
// EMBED_BASE_URL overrides the committed embedBaseUrl, the same way DOXA_SYNC_PATHS extends paths:
|
||||
// a host that builds its own tree (maps-dev's /cdn build inherits the container env) bakes absolute
|
||||
// snippet URLs without a committed config change. It is the base WITHOUT the tree name
|
||||
// (https://maps-dev.nuxtbase.app/cdn -> .../cdn/<name>/...). check-embed-targets-exist reads the same.
|
||||
// (https://<host>/cdn -> .../cdn/<name>/...). check-embed-targets-exist reads the same.
|
||||
const embedBaseUrl = ((process.env.EMBED_BASE_URL || '').trim()
|
||||
|| (typeof cfg.embedBaseUrl === 'string' ? cfg.embedBaseUrl.trim() : '')).replace(/\/+$/, '')
|
||||
const tokenUrl = typeof cfg.tokenUrl === 'string' ? cfg.tokenUrl.trim() : ''
|
||||
|
|
@ -857,9 +857,9 @@ const MAPLIBRE_STYLE_ORIGIN = 'https://tiles.openfreemap.org' // useMapInstanc
|
|||
* token; the hint follows the build default and the report names that miss case (one unused preconnect).
|
||||
*/
|
||||
function engineFor({ config }) {
|
||||
// CORRECTED 2026-09-29 (regions executed the shipping resolver; rega's A9NET confirmed on the wire):
|
||||
// CORRECTED 2026-09-29 (the shipping resolver was executed, and a network trace confirmed it on the wire):
|
||||
// a token no longer selects Mapbox. useMapProvider's default is MapLibre unless provider is EXPLICITLY
|
||||
// 'mapbox'. rega measured all 8 auto leaves — which DO carry a pray.doxa.life token endpoint — loading
|
||||
// 'mapbox'. A browser check measured all 8 auto leaves — which DO carry a pray.doxa.life token endpoint — loading
|
||||
// MapLibre from jsdelivr cold. So the hint's engine is Mapbox ONLY when provider:'mapbox' is set; every
|
||||
// other case (auto, or provider:'maplibre') is MapLibre, regardless of any baked token / tokenUrl.
|
||||
return String((config && config.provider) || '').toLowerCase().trim() === 'mapbox' ? 'mapbox' : 'maplibre'
|
||||
|
|
@ -886,7 +886,7 @@ function resourceHints({ isMapLibre, config, apiBaseUrl }) {
|
|||
const gl = engine === 'maplibre' ? CDN.maplibreGlJs : CDN.mapboxGlJs
|
||||
lines.push(` <link rel="preload" as="script" href="${gl.url}" integrity="${gl.integrity}" crossorigin="anonymous" />`)
|
||||
}
|
||||
return lines.length ? ` <!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (dx-base/a9-leaf-hints; engine = provider or the MapLibre default). -->\n${lines.join('\n')}\n` : ''
|
||||
return lines.length ? ` <!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->\n${lines.join('\n')}\n` : ''
|
||||
}
|
||||
|
||||
// Does this BUILT bundle .js reference a map engine? Read from public/<tree>/<bundle>/<bundle>.js with the
|
||||
|
|
@ -957,14 +957,14 @@ function mapPageHtml(opts) {
|
|||
// EVERY non-map leaf (cap-4 composites + cap-1/2/3 non-map leaves), not just cap-5 shells.
|
||||
const shellOnly = !!shellTokenFree || notAMap
|
||||
const tokenFree = isMapLibre || shellOnly
|
||||
// A9 leaf hints (dx-base/a9-leaf-hints): give the browser the map's origins BEFORE the bundle
|
||||
// A9 leaf hints: give the browser the map's origins BEFORE the bundle
|
||||
// parses. Engine-aware, derived from the SAME inputs the runtime ladder reads (below), never
|
||||
// hardcoded: preconnect the token-implied engine + the style host + the data origin, and — only
|
||||
// where the leaf emits no engine <script> today (the 'auto' leaf) — preload the engine, its URL,
|
||||
// integrity and crossorigin from library/constants/cdn.js so it cannot drift from the runtime tag.
|
||||
// WHO GETS HINTS: any page whose OWN bundle loads a map engine — the directly verifiable signal
|
||||
// (`bundleLoadsEngine`, an engine reference in the built .js), NOT the leaf's `isMap` flag. rega's A9
|
||||
// net run found regions-mfe (isMap:false, 56 engine refs in its bundle) AND research-map-plus-grid
|
||||
// (`bundleLoadsEngine`, an engine reference in the built .js), NOT the leaf's `isMap` flag. A network
|
||||
// run found regions-mfe (isMap:false, 56 engine refs in its bundle) AND research-map-plus-grid
|
||||
// (isMap:false, own bundle 0 refs but composes doxa-research-map, 74 refs) opening cdn.jsdelivr.net +
|
||||
// tiles.openfreemap.org from the top page and getting no hints — my first cut keyed on isMap and missed
|
||||
// both, the same trap check:leaf-engine-per-provider's isMap gate fell into. bundleLoadsEngine is true
|
||||
|
|
@ -1061,7 +1061,7 @@ ${hintBlock} ${peerBlock}
|
|||
@media print {
|
||||
html, body { height: auto; background: #fff; }
|
||||
#status:not(.err) { display: none; }
|
||||
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
|
||||
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
|
||||
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
|
||||
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
|
||||
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
|
||||
|
|
@ -1310,7 +1310,7 @@ function instanceHtml(b, profile, inst, tokenUrlExpr, bakedToken) {
|
|||
return mapPageHtml({
|
||||
title: `${inst.title} · ${b.bundle}`,
|
||||
tag,
|
||||
// MERGE, never replace (go-int: a preset lost its profile's pinHint:false): the profile's
|
||||
// MERGE, never replace (a preset once lost its profile's pinHint:false): the profile's
|
||||
// <profile>.meta.json `config` first, then the instance's props over it. The rule is stated in
|
||||
// documentation/PRESET-PLUGIN-STANDARD.md and asserted by check:preset-config-merge.
|
||||
config: { assetsBase: assetsBaseFor(4), ...(profile.extraConfig || {}), ...mobileConfig(profile), ...inst.props },
|
||||
|
|
@ -1724,7 +1724,7 @@ function writeManifestAndHtml(root, bundles, tokenUrlExpr, bakedToken) {
|
|||
// THE FRONT PAGE IS THE STUDIO (see studioRootHtml). The staging-forge chooser that used to be
|
||||
// written here (rootHtml) is retired with it; nothing is written above the tree any more.
|
||||
// …with the studio bundle's integrity, written HERE: it used to be added only by fix:sri, so running
|
||||
// `bun run tree` after fix:sri stripped it again (rega, 2026-09-25).
|
||||
// `bun run tree` after fix:sri stripped it again (found 2026-09-25).
|
||||
writeFileSync(join(root, 'index.html'), studioRootHtml(bakedToken, (bundles.find((b) => b.bundle === 'plugin-studio') || {}).integrity || ''))
|
||||
|
||||
|
||||
|
|
@ -2019,7 +2019,7 @@ function main() {
|
|||
}
|
||||
// people-groups/lists/*.json — the ?list= / profile-config `list` prop fetches the chosen list by name
|
||||
// (frontier-500k, frontier, unreached, unengaged), so the whole folder ships. Missing, every non-default
|
||||
// list 404'd on the built tree and drew 0 pins (go-int / rega at a71007f1).
|
||||
// list 404'd on the built tree and drew 0 pins (measured at a71007f1).
|
||||
const listsSrc = join(pubAssets, 'people-groups', 'lists')
|
||||
if (existsSync(listsSrc)) {
|
||||
const listsDest = join(treeAssets, 'people-groups', 'lists')
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* profile-meta.mjs — the per-profile fields a host reads from <profile>.meta.json, in ONE place.
|
||||
*
|
||||
* generate-tree (the built manifest.json) and the dev server's /__plugin-gallery/manifest.json both call
|
||||
* this, so the dev Studio and the built Studio cannot disagree (rega: the corner widget never mounted in
|
||||
* this, so the dev Studio and the built Studio cannot disagree (the corner widget once never mounted in
|
||||
* dev because only the built manifest carried "widget").
|
||||
* stage "production" | "experimental" default experimental
|
||||
* heavy true when the element holds live WebGL contexts
|
||||
|
|
@ -16,7 +16,7 @@ import { join } from 'node:path'
|
|||
const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left']
|
||||
|
||||
/**
|
||||
* ★ WHO ELSE MAY LIST THIS PLUGIN (dx-base, 2026-09-28). The platform dashboard reads each team's
|
||||
* ★ WHO ELSE MAY LIST THIS PLUGIN (2026-09-28). The platform dashboard reads each team's
|
||||
* own /cdn/dx-maps/manifest.json instead of one shared index, so this field is what decides whether
|
||||
* a plugin appears in ANOTHER team's library (loaded read-only from the owner's CDN).
|
||||
*
|
||||
|
|
|
|||
121
library/components/poster/PanelSheets.vue
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<!--
|
||||
PanelSheets.vue — SHARED. A preview of a large poster as the sheets it prints on: the sheets pulled apart, each
|
||||
join's under-strip drawn as a red "glue here" band, the trimmed edges and the "s / N" tags, the way a reader
|
||||
will assemble it on the wall. The geometry is panelSheets.js (the same plan the print uses).
|
||||
|
||||
- Give it `src` (the finished poster as one image or SVG URL) and each sheet shows its own slice of it, cut with
|
||||
CSS (an overflow:hidden box, the image sized to the whole poster and offset by the sheet's place). Or use
|
||||
the scoped slot #sheet="{ s, slice, band }" to draw a sheet yourself.
|
||||
- It fills its container's width; its height follows the layout's aspect. The container decides the size.
|
||||
- It handles no pointer events (pointer-events:none throughout), so a card can make the preview its control.
|
||||
- It emits ONE `load` when `src` has loaded, or ONE `error` when it cannot, per src.
|
||||
- The red band is always drawn here, even for a poster whose print BLEEDS under the join: a preview's job is
|
||||
to show how the pieces go together.
|
||||
Styles go in through useShadowStyles, because it renders inside a plugin's shadow root.
|
||||
-->
|
||||
<template>
|
||||
<div class="pnl" :style="{ aspectRatio: aspect }" aria-hidden="true">
|
||||
<div v-for="sh in view" :key="sh.s" class="pnl-sheet" :style="sh.box">
|
||||
<img v-if="src && !failed" class="pnl-img" :src="src" alt="" draggable="false" :style="sh.img">
|
||||
<slot name="sheet" :s="sh.s" :slice="sh.slice" :band="sh.band"></slot>
|
||||
<svg class="pnl-marks" :viewBox="`0 0 ${plan.sheetW} ${plan.sheetH}`" preserveAspectRatio="none">
|
||||
<g v-if="sh.band">
|
||||
<rect :x="sh.band.x" :y="sh.band.y" :width="sh.band.w" :height="sh.band.h" fill="rgba(176,0,0,.14)" />
|
||||
<line v-bind="sh.bandEdge" stroke="#b00" :stroke-width="stroke" :stroke-dasharray="`${stroke * 4} ${stroke * 3}`" />
|
||||
</g>
|
||||
<line v-if="sh.trimEdge" v-bind="sh.trimEdge" stroke="#999" :stroke-width="stroke" />
|
||||
</svg>
|
||||
<!-- Text in HTML, not in the stretched SVG, so it keeps its proportions; sized in the sheet's own units. -->
|
||||
<div v-if="sh.band" class="pnl-glue" :class="{ h: horizontal }" :style="sh.bandBox">
|
||||
<span :style="{ fontSize: sh.labelFs }">{{ glueHere }} <svg v-if="horizontal" class="pnl-arr" viewBox="0 0 10 10"><path d="M1 5 9 1v8z" fill="currentColor"/></svg><template v-else>▲</template> <bdi dir="ltr">{{ sh.s }} / {{ plan.count }}</bdi></span>
|
||||
</div>
|
||||
<div class="pnl-tag" dir="ltr" :style="{ fontSize: sh.tagFs }">{{ sh.s + 1 }} / {{ plan.count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { planPanels, sliceFor, screenLayout } from './panelSheets.js'
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
import { useOptionalT } from '../../composables/useOptionalT.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** One printed sheet, in inches. */
|
||||
sheetW: { type: Number, required: true },
|
||||
sheetH: { type: Number, required: true },
|
||||
count: { type: Number, default: 1 },
|
||||
direction: { type: String, default: 'vertical' }, // 'vertical' (stacked) | 'horizontal' (side by side)
|
||||
/** How far each sheet slides under the one before it (inches). */
|
||||
joinIn: { type: Number, default: 0 },
|
||||
/** The gap drawn between sheets, in inches (the Bless wall uses 0.75). */
|
||||
gapIn: { type: Number, default: 0.75 },
|
||||
/** The finished poster as one image (webp/png/svg URL). Optional: the #sheet slot can draw instead. */
|
||||
src: { type: String, default: '' },
|
||||
/** Translated strings: { glueHere }. Without one, the catalogue's poster.glue_here in the reader's language. */
|
||||
labels: { type: Object, default: () => ({}) },
|
||||
})
|
||||
const emit = defineEmits(['load', 'error'])
|
||||
|
||||
// The preview shows the finished poster continuing under each band ('bleed'), so a sliced image lines up.
|
||||
const plan = computed(() => planPanels({ sheetW: props.sheetW, sheetH: props.sheetH, count: props.count,
|
||||
direction: props.direction, joinIn: props.joinIn, joinFill: 'bleed' }))
|
||||
const horizontal = computed(() => plan.value.direction === 'horizontal')
|
||||
const layout = computed(() => screenLayout(plan.value, { gapIn: props.gapIn, pxPerIn: 1 }))
|
||||
const aspect = computed(() => `${layout.value.worldW} / ${layout.value.worldH}`)
|
||||
const t = useOptionalT('poster')
|
||||
const glueHere = computed(() => props.labels.glueHere || t('poster.glue_here'))
|
||||
// Marks scale with the sheet: a line ~1/300 of the sheet's short side, text from the band's depth.
|
||||
const stroke = computed(() => Math.min(plan.value.sheetW, plan.value.sheetH) / 300)
|
||||
const pct = (v, of) => `${(v / of) * 100}%`
|
||||
|
||||
const view = computed(() => {
|
||||
const P = plan.value, L = layout.value, F = P.finished
|
||||
return L.sheets.map((r) => {
|
||||
const sh = P.sheets[r.s], slice = sliceFor(P, r.s), b = sh.band
|
||||
const H = P.direction === 'horizontal'
|
||||
// font sizes as a share of the SHEET's width, via container query units (the sheet is the container)
|
||||
const cq = (inches) => `${(inches / P.sheetW) * 100}cqw`
|
||||
return {
|
||||
s: r.s, slice, band: b,
|
||||
box: { left: pct(r.x, L.worldW), top: pct(r.y, L.worldH), width: pct(r.w, L.worldW), height: pct(r.h, L.worldH) },
|
||||
// the whole finished poster behind this sheet, shifted so the sheet's slice shows
|
||||
img: { width: pct(F.w, P.sheetW), height: pct(F.h, P.sheetH), left: pct(-sh.at.x, P.sheetW), top: pct(-sh.at.y, P.sheetH) },
|
||||
bandBox: b ? { left: pct(b.x, P.sheetW), top: pct(b.y, P.sheetH), width: pct(b.w, P.sheetW), height: pct(b.h, P.sheetH) } : null,
|
||||
bandEdge: b ? (H ? { x1: b.w, y1: 0, x2: b.w, y2: P.sheetH } : { x1: 0, y1: b.h, x2: P.sheetW, y2: b.h }) : null,
|
||||
trimEdge: sh.trimmed ? (H ? { x1: P.sheetW, y1: 0, x2: P.sheetW, y2: P.sheetH } : { x1: 0, y1: P.sheetH, x2: P.sheetW, y2: P.sheetH }) : null,
|
||||
labelFs: cq(Math.min(P.joinIn * 0.16, P.sheetW * 0.03)),
|
||||
tagFs: cq(Math.max(0.35, Math.min(P.sheetW, P.sheetH) * 0.02)),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ONE load or error per src, from one probe image (the per-sheet <img>s share its cached response). A src that
|
||||
// fails shows blank sheets, never the browser's broken-image icon; the card says why.
|
||||
const failed = ref(false)
|
||||
let probe = null
|
||||
watch(() => props.src, (src) => {
|
||||
if (probe) { probe.onload = probe.onerror = null; probe = null }
|
||||
failed.value = false
|
||||
if (!src || typeof Image === 'undefined') return
|
||||
const img = new Image()
|
||||
probe = img
|
||||
img.onload = () => { if (probe === img) { probe = null; emit('load') } }
|
||||
img.onerror = () => { if (probe === img) { probe = null; failed.value = true; emit('error') } }
|
||||
img.src = src
|
||||
}, { immediate: true })
|
||||
onBeforeUnmount(() => { if (probe) { probe.onload = probe.onerror = null; probe = null } })
|
||||
|
||||
useShadowStyles(
|
||||
'.pnl{position:relative;width:100%;pointer-events:none;user-select:none}' +
|
||||
'.pnl-sheet{position:absolute;overflow:hidden;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.28);container-type:inline-size}' +
|
||||
'.pnl-img{position:absolute;max-width:none;display:block;object-fit:fill}' +
|
||||
'.pnl-marks{position:absolute;inset:0;width:100%;height:100%;overflow:visible}' +
|
||||
'.pnl-glue{position:absolute;display:flex;align-items:center;justify-content:center;overflow:hidden}' +
|
||||
'.pnl-glue span{color:#b00;font:700 1em/1.1 system-ui,-apple-system,"Segoe UI",sans-serif;letter-spacing:.05em;text-transform:uppercase;white-space:nowrap;background:rgba(255,255,255,.82);padding:.1em .35em;border-radius:.2em}' +
|
||||
'.pnl-glue.h span{writing-mode:vertical-rl}' +
|
||||
'.pnl-arr{width:.8em;height:.8em;vertical-align:middle;display:inline-block}' +
|
||||
'.pnl-tag{position:absolute;top:1.5%;right:2%;color:#999;font:600 1em/1 system-ui,-apple-system,"Segoe UI",sans-serif}',
|
||||
'panel-sheets'
|
||||
)
|
||||
</script>
|
||||
|
|
@ -80,6 +80,10 @@ const props = defineProps({
|
|||
itemCountKey: { type: String, default: '' },
|
||||
collapsible: { type: Boolean, default: false },
|
||||
collapsedBy: { type: Function, default: () => false },
|
||||
// A section's people-group total, shown in its header and used to order the sections (largest first).
|
||||
// Default: the sum of its cards' counts. A caller whose cards overlap overrides it, so a group is counted
|
||||
// once: the Country Posters India section holds India's own card AND one card per state, the same 855 groups.
|
||||
sectionTotal: { type: Function, default: null },
|
||||
// A card may declare itself unsupported (India: 855 groups do not fit a sheet).
|
||||
// The caller supplies the reason; the wall renders it instead of a poster.
|
||||
unsupported: { type: Function, default: () => null },
|
||||
|
|
@ -185,7 +189,23 @@ const props = defineProps({
|
|||
// cards are thumbnails can set it lower so a phone keeps two per row; see the clamp below for why
|
||||
// that is safe now.
|
||||
minCardPx: { type: Number, default: 0 },
|
||||
// ★ WHAT THE CARD'S HEADER STICKER DOES. null (the default) is today's "Show on map" toggle,
|
||||
// unchanged for every wall that does not set it. A wall with no map beside it (a gallery of
|
||||
// templates) passes { labelKey, ariaKey } instead: the sticker then reads t(labelKey), is a plain
|
||||
// button rather than a toggle (no aria-pressed, no is-last highlight), and emits 'header-action'
|
||||
// with the item, dispatching no window event. ariaKey gets { label: item.label }. An item with no
|
||||
// count (a template, not a list of people) shows the label alone, without the "N UPG ·" prefix.
|
||||
headerAction: { type: Object, default: null },
|
||||
// ★ WHERE A CARD'S PREVIEW IMAGE COMES FROM. null (the default) is the stored
|
||||
// previews/<id>.webp every wall uses today, untouched. A wall whose cards have no stored
|
||||
// preview (a gallery of templates drawn from the library, so a library change shows up in it)
|
||||
// passes async (item) => an image URL | null. It runs only for cards that come into view, one at
|
||||
// a time, cached per page by instanceId + item id; until it answers the card shows its label, and
|
||||
// null, a throw or no answer within PREVIEW_TIMEOUT_MS shows the "preview unavailable" placeholder.
|
||||
// The page keeps each returned URL and never revokes it (see GENERATED PREVIEWS below).
|
||||
previewSrc: { type: Function, default: null },
|
||||
})
|
||||
const emit = defineEmits(['header-action'])
|
||||
|
||||
// The live count, seeded from the prop. A ref rather than the prop directly, because the control
|
||||
// changes it and a prop is not ours to write.
|
||||
|
|
@ -314,6 +334,8 @@ const fallbackBase = computed(() => {
|
|||
const attempt = ref({})
|
||||
const previewUrl = (id) => `${attempt.value[id] ? fallbackBase.value : base.value}/previews/${id}.webp`
|
||||
function onPreviewError(id) {
|
||||
// A generated preview has no second base to try.
|
||||
if (props.previewSrc) { missing.value = { ...missing.value, [id]: true }; return }
|
||||
const fb = fallbackBase.value
|
||||
if (!attempt.value[id] && fb && fb !== base.value) { attempt.value = { ...attempt.value, [id]: 1 }; return }
|
||||
missing.value = { ...missing.value, [id]: true }
|
||||
|
|
@ -333,7 +355,7 @@ const sections = computed(() => {
|
|||
.map(([name, items]) => ({
|
||||
name,
|
||||
items: items.slice().sort((a, b) => (b.count || 0) - (a.count || 0)),
|
||||
total: items.reduce((n, x) => n + (x.count || 0), 0),
|
||||
total: props.sectionTotal ? props.sectionTotal(name, items) : items.reduce((n, x) => n + (x.count || 0), 0),
|
||||
}))
|
||||
.sort((a, b) => b.total - a.total)
|
||||
})
|
||||
|
|
@ -419,6 +441,81 @@ onMounted(() => {
|
|||
onUpdated(observeRows)
|
||||
onBeforeUnmount(() => { if (rowRO) rowRO.disconnect(); rowRO = null })
|
||||
|
||||
// ─── GENERATED PREVIEWS (the previewSrc prop) ────────────────────────────────
|
||||
// Nothing here runs unless the caller passes previewSrc. A pending card registers its placeholder
|
||||
// through a function ref; the observer queues it once it is in view; the queue renders ONE preview
|
||||
// at a time, so a gallery never starts a burst of renders on load. The answer is cached on the page
|
||||
// (a module cache would be per bundle), so a remount or a second tab shows it without a re-render.
|
||||
// Two limits, both deliberate: (1) the timeout stops the WAIT, not the render, so after one times out
|
||||
// the next may start while the slow one still runs ("one at a time" is best-effort past a timeout);
|
||||
// (2) the cache keeps whatever URL previewSrc returns for the life of the page and never revokes it,
|
||||
// which suits a gallery that shows the same few previews again; a caller that makes many should say so.
|
||||
const PREVIEW_TIMEOUT_MS = 20000
|
||||
const previewCache = (globalThis.__posterWallPreviews_v1 ||= new Map())
|
||||
const generated = ref({}) // id -> the URL previewSrc answered
|
||||
const previewEls = new Map() // id -> its pending placeholder
|
||||
const previewIds = new WeakMap() // placeholder -> id
|
||||
const previewQueue = []
|
||||
let previewBusy = false
|
||||
let previewIO = null
|
||||
|
||||
function setPreviewEl(id, el) {
|
||||
const old = previewEls.get(id)
|
||||
if (old === el) return
|
||||
if (old && previewIO) previewIO.unobserve(old)
|
||||
if (!el) { previewEls.delete(id); return }
|
||||
previewEls.set(id, el)
|
||||
previewIds.set(el, id)
|
||||
if (previewIO) previewIO.observe(el)
|
||||
}
|
||||
function queuePreview(id) {
|
||||
if (id == null || generated.value[id] || missing.value[id] || previewQueue.includes(id)) return
|
||||
previewQueue.push(id)
|
||||
pumpPreviews()
|
||||
}
|
||||
function withTimeout(promise, ms) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('preview timed out')), ms)
|
||||
promise.then((v) => { clearTimeout(timer); resolve(v) }, (e) => { clearTimeout(timer); reject(e) })
|
||||
})
|
||||
}
|
||||
async function pumpPreviews() {
|
||||
if (previewBusy || !previewQueue.length) return
|
||||
const id = previewQueue.shift()
|
||||
const item = (props.items || []).find((x) => x && x.id === id)
|
||||
if (!item || !props.previewSrc) { pumpPreviews(); return }
|
||||
previewBusy = true
|
||||
const key = `${props.instanceId}\u0000${id}`
|
||||
try {
|
||||
if (!previewCache.has(key)) {
|
||||
previewCache.set(key, withTimeout(Promise.resolve().then(() => props.previewSrc(item)), PREVIEW_TIMEOUT_MS))
|
||||
}
|
||||
let url = null
|
||||
try { url = await previewCache.get(key) } catch (_) { url = null }
|
||||
if (url) generated.value = { ...generated.value, [id]: url }
|
||||
else {
|
||||
previewCache.delete(key) // a later mount may try again
|
||||
missing.value = { ...missing.value, [id]: true }
|
||||
}
|
||||
} finally {
|
||||
previewBusy = false
|
||||
pumpPreviews()
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
if (!props.previewSrc) return
|
||||
// No observer (an old engine): still one at a time, just not deferred to view.
|
||||
if (typeof IntersectionObserver === 'undefined') { for (const id of previewEls.keys()) queuePreview(id); return }
|
||||
// The ROOT is the wall's own scroller: a pane wall scrolls inside .wall, which clips its cards, so
|
||||
// a viewport root would never see a card ahead of view and the margin would buy nothing. An embedded
|
||||
// wall is in normal flow and the page scrolls it, so there the viewport is the right root.
|
||||
previewIO = new IntersectionObserver((entries) => {
|
||||
for (const e of entries) if (e.isIntersecting) queuePreview(previewIds.get(e.target))
|
||||
}, { root: props.embedded ? null : wallEl.value, rootMargin: '200px' })
|
||||
for (const el of previewEls.values()) previewIO.observe(el)
|
||||
})
|
||||
onBeforeUnmount(() => { if (previewIO) previewIO.disconnect(); previewIO = null; previewQueue.length = 0 })
|
||||
|
||||
function showOnMap(item) {
|
||||
// TOGGLE, not a one-way set: a control the user can turn on but not off reads as
|
||||
// broken even when the selection is correct.
|
||||
|
|
@ -1255,12 +1352,21 @@ useShadowStyles(
|
|||
|
||||
<!-- A TOGGLE (showOnMap clears the selection on a second press), so it says whether it is on:
|
||||
without aria-pressed a second press sounded identical to the first to a screen reader. -->
|
||||
<button type="button" class="tape tape-h" :aria-pressed="lastShown === item.id ? 'true' : 'false'"
|
||||
<button v-if="!headerAction" type="button" class="tape tape-h" :aria-pressed="lastShown === item.id ? 'true' : 'false'"
|
||||
:aria-label="t('buttons.showOnMapAria', { label: item.label })" @click="showOnMap(item)">
|
||||
<span class="hcount">{{ item.count }} {{ unit }}</span>
|
||||
<span class="hsep">·</span>
|
||||
<span class="hlabel">{{ t('buttons.showOnMap') }}</span>
|
||||
</button>
|
||||
<!-- headerAction set: a plain button that hands the item to the host (see the prop). -->
|
||||
<button v-else type="button" class="tape tape-h"
|
||||
:aria-label="t(headerAction.ariaKey || headerAction.labelKey, { label: item.label })" @click="emit('header-action', item)">
|
||||
<template v-if="item.count != null">
|
||||
<span class="hcount">{{ item.count }} {{ unit }}</span>
|
||||
<span class="hsep">·</span>
|
||||
</template>
|
||||
<span class="hlabel">{{ t(headerAction.labelKey) }}</span>
|
||||
</button>
|
||||
|
||||
<div class="main">
|
||||
<!-- Unsupported comes FIRST: there is no poster to try to load. -->
|
||||
|
|
@ -1271,8 +1377,14 @@ useShadowStyles(
|
|||
</div>
|
||||
<!-- previewAction='print': the poster preview IS the control. Keyboard-
|
||||
reachable (role/tabindex/Enter/Space), not click-only. -->
|
||||
<!-- previewSrc set and the preview not arrived yet: the card's label, never a spinner. -->
|
||||
<div v-else-if="previewSrc && !generated[item.id] && !missing[item.id]" class="ph is-pending"
|
||||
:ref="(el) => setPreviewEl(item.id, el)">
|
||||
<div class="ph-label">{{ item.label }}</div>
|
||||
<div v-if="item.count" class="ph-count">{{ countOf('wall.peopleGroupCount', item.count) }}</div>
|
||||
</div>
|
||||
<img v-else-if="!missing[item.id]" class="img"
|
||||
:class="{ 'is-clickable': previewIsControl }" :src="previewUrl(item.id)"
|
||||
:class="{ 'is-clickable': previewIsControl }" :src="previewSrc ? generated[item.id] : previewUrl(item.id)"
|
||||
:alt="previewIsControl
|
||||
? hud('wall.posterAltAction', '{label} — prayer poster (click to preview and print)', { label: item.label })
|
||||
: hud('wall.posterAlt', '{label} — prayer poster', { label: item.label })"
|
||||
|
|
|
|||
149
library/components/poster/panelSheets.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* panelSheets.js — SHARED. The geometry of a large poster printed as several sheets and joined on the wall,
|
||||
* and the marks that tell the print shop how to join them. Pure: numbers and HTML/CSS strings, no Vue.
|
||||
*
|
||||
* A poster too big for one sheet is printed as `count` sheets of sheetW × sheetH inches, joined in a line
|
||||
* (`direction` 'vertical': stacked top to bottom; 'horizontal': side by side). At each join the EARLIER sheet's
|
||||
* edge is trimmed flat and laid over the first `joinIn` inches of the next one, so
|
||||
* finished length = count × sheet − (count − 1) × joinIn (3 × 36 − 2 × 6 = 96; 2 × 36 − 6 = 66).
|
||||
* The strip that ends up underneath (the JOIN BAND) is filled one of two ways, chosen by the renderer:
|
||||
* · 'glue' — left blank and marked "glue here" in red; content is fitted around it, so nothing crosses a
|
||||
* join (the Bless wall's cards);
|
||||
* · 'bleed' — the poster simply continues under it, so a trim a few millimetres off shows poster, never a
|
||||
* white seam (a map, whose title and land cross the join). Only a thin guide and a small tag
|
||||
* are printed there: a solid band would show through a misaligned join.
|
||||
* On SCREEN (PanelSheets.vue, and any preview) the band is always drawn red with the sheets pulled apart, so a
|
||||
* reader sees how the pieces go together, whichever way the print fills it.
|
||||
*
|
||||
* The parameter names are the size table's `sheets` object (library/constants/posterSizes.js), so a consumer
|
||||
* can call planPanels(SIZES[key].sheets); borderIn and joinFill belong to the renderer, not the size.
|
||||
* Lengths are inches unless a name says px.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {{ sheetW:number, sheetH:number, count?:number, direction?:'vertical'|'horizontal', joinIn?:number,
|
||||
* borderIn?:number, joinFill?:'glue'|'bleed' }} o
|
||||
* @returns the plan: finished size, and per sheet where it sits in the finished poster, its join band (sheet
|
||||
* coordinates, null on the first sheet), whether its trailing edge is trimmed, and its CONTENT box (sheet
|
||||
* coordinates): the outer border on the poster's outer edges, the join band on a joined leading edge, and
|
||||
* nothing on a trimmed trailing edge (that edge lies over the next sheet's join band).
|
||||
*/
|
||||
export function planPanels(o) {
|
||||
const sheetW = +o.sheetW, sheetH = +o.sheetH
|
||||
const count = Math.max(1, Math.round(+o.count || 1))
|
||||
const direction = o.direction === 'horizontal' ? 'horizontal' : 'vertical'
|
||||
const joinIn = count > 1 ? Math.max(0, +o.joinIn || 0) : 0
|
||||
const borderIn = Math.max(0, +o.borderIn || 0)
|
||||
const joinFill = o.joinFill === 'bleed' ? 'bleed' : 'glue'
|
||||
if (!(sheetW > 0 && sheetH > 0)) throw new Error('planPanels: sheetW and sheetH must be positive inches')
|
||||
const along = direction === 'vertical' ? sheetH : sheetW
|
||||
if (joinIn >= along) throw new Error('planPanels: joinIn must be smaller than the sheet along the join')
|
||||
const H = direction === 'vertical'
|
||||
const sheets = []
|
||||
for (let s = 0; s < count; s++) {
|
||||
const first = s === 0, last = s === count - 1
|
||||
const off = s * (along - joinIn) // this sheet's leading edge in the finished poster
|
||||
const at = H ? { x: 0, y: off } : { x: off, y: 0 }
|
||||
const band = first ? null : H ? { x: 0, y: 0, w: sheetW, h: joinIn } : { x: 0, y: 0, w: joinIn, h: sheetH }
|
||||
const lead = first ? borderIn : joinIn, trail = last ? borderIn : 0
|
||||
const content = H
|
||||
? { x0: borderIn, y0: lead, x1: sheetW - borderIn, y1: sheetH - trail }
|
||||
: { x0: lead, y0: borderIn, x1: sheetW - trail, y1: sheetH - borderIn }
|
||||
sheets.push({ s, first, last, at, band, trimmed: !last && count > 1, content })
|
||||
}
|
||||
const finished = H
|
||||
? { w: sheetW, h: count * sheetH - (count - 1) * joinIn }
|
||||
: { w: count * sheetW - (count - 1) * joinIn, h: sheetH }
|
||||
return { sheetW, sheetH, count, direction, joinIn, borderIn, joinFill, finished, sheets }
|
||||
}
|
||||
|
||||
/**
|
||||
* The part of the FINISHED poster that sheet s (0-based: 0 … count − 1) prints, and where on the sheet it goes (inches). With 'bleed' the
|
||||
* whole sheet is poster (its band repeats what the sheet before shows); with 'glue' the band is left out.
|
||||
* A renderer draws the finished poster once and places this rect of it on page s (the per-sheet PDF page).
|
||||
*/
|
||||
export function sliceFor(plan, s) {
|
||||
const sh = plan.sheets[s]
|
||||
if (!sh) throw new Error(`sliceFor: no sheet ${s}; sheets are 0-based, 0..${plan.count - 1}`)
|
||||
const skip = plan.joinFill === 'glue' && sh.band ? plan.joinIn : 0
|
||||
const V = plan.direction === 'vertical'
|
||||
return V
|
||||
? { x: sh.at.x, y: sh.at.y + skip, w: plan.sheetW, h: plan.sheetH - skip, sheetX: 0, sheetY: skip }
|
||||
: { x: sh.at.x + skip, y: sh.at.y, w: plan.sheetW - skip, h: plan.sheetH, sheetX: skip, sheetY: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* The sheets laid out for a SCREEN preview, pulled apart by gapIn so the joins show: stacked (vertical) or side
|
||||
* by side (horizontal), in px at pxPerIn. `step` is the distance between sheet origins in inches.
|
||||
*/
|
||||
export function screenLayout(plan, { gapIn = 0.75, pxPerIn = 96 } = {}) {
|
||||
const V = plan.direction === 'vertical'
|
||||
const step = (V ? plan.sheetH : plan.sheetW) + gapIn
|
||||
const sheets = plan.sheets.map((sh) => V
|
||||
? { s: sh.s, x: 0, y: sh.s * step * pxPerIn, w: plan.sheetW * pxPerIn, h: plan.sheetH * pxPerIn }
|
||||
: { s: sh.s, x: sh.s * step * pxPerIn, y: 0, w: plan.sheetW * pxPerIn, h: plan.sheetH * pxPerIn })
|
||||
const n = plan.count
|
||||
return {
|
||||
step, gapIn, pxPerIn, sheets,
|
||||
worldW: (V ? plan.sheetW : n * plan.sheetW + (n - 1) * gapIn) * pxPerIn,
|
||||
worldH: (V ? n * plan.sheetH + (n - 1) * gapIn : plan.sheetH) * pxPerIn,
|
||||
}
|
||||
}
|
||||
|
||||
// A left-pointing arrow as a shape: vertical text rotates arrow CHARACTERS, never an inline SVG.
|
||||
export const LEFT_ARROW = '<svg viewBox="0 0 10 10" width="0.8em" height="0.8em" style="vertical-align:middle"><path d="M1 5 9 1v8z" fill="currentColor"/></svg>'
|
||||
|
||||
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
||||
const inch = (v) => v.toFixed(3) + 'in'
|
||||
|
||||
/**
|
||||
* The join marks for sheet s (0-based), as HTML to place inside the sheet's own positioned box (class "sheet"):
|
||||
* before — the join band ("glue here ▲ s / N"), drawn first so content paints over nothing it needs;
|
||||
* after — the trim line on a trimmed edge, and the "s / N" sheet tag in the corner.
|
||||
* `screen: true` always draws the red band (a preview shows how the sheets join, even for a bleed print).
|
||||
* glueHere: the translated "glue here" (default English; the Bless wall passes wall.glueHere).
|
||||
*/
|
||||
export function panelChromeHtml(plan, s, { glueHere = 'glue here', screen = false } = {}) {
|
||||
const sh = plan.sheets[s], N = plan.count, V = plan.direction === 'vertical'
|
||||
let before = ''
|
||||
if (sh.band) {
|
||||
// The arrow points at the sheet laid over this band: the one above, or (side by side) the one to its left.
|
||||
// (In the side-by-side band the label runs top to bottom, which would turn a ◀ character on its side; the
|
||||
// arrow is drawn instead, so it points left in every browser.)
|
||||
const label = V ? `${esc(glueHere)} ▲ ${s} / ${N}`
|
||||
: `${esc(glueHere)} ${LEFT_ARROW} <bdi dir="ltr">${s} / ${N}</bdi>`
|
||||
if (plan.joinFill === 'glue' || screen) {
|
||||
before = V
|
||||
? `<div class="glue" style="top:0;height:${inch(plan.joinIn)}">${label}</div>`
|
||||
: `<div class="glue glue-h" style="left:0;width:${inch(plan.joinIn)}">${label}</div>`
|
||||
} else {
|
||||
before = `<div class="glue-guide${V ? '' : ' glue-h'}"><span>${label}</span></div>`
|
||||
}
|
||||
}
|
||||
const trim = sh.trimmed ? (V ? '<div class="trim"></div>' : '<div class="trim trim-h"></div>') : ''
|
||||
return { before, after: `${trim}<div class="tag">${s + 1} / ${N}</div>` }
|
||||
}
|
||||
|
||||
/**
|
||||
* The CSS for panelChromeHtml's marks. The vertical rules are the Bless poster's own, byte for byte (its print
|
||||
* is pinned by check:wall-wysiwyg and check:panel-sheets). The label and tag scale with the join band (the
|
||||
* border on a single sheet).
|
||||
*/
|
||||
export function panelCss(plan) {
|
||||
const j = plan.joinIn || plan.borderIn, b = plan.borderIn
|
||||
let css = `.glue{position:absolute;left:0;width:100%;border-bottom:.02in dashed #b00;display:flex;align-items:center;justify-content:center;font:700 ${inch(Math.min(0.5, j * 0.08))} sans-serif;color:#b00;background:rgba(176,0,0,.035);letter-spacing:.05em;text-transform:uppercase}
|
||||
.trim{position:absolute;left:0;bottom:0;width:100%;border-bottom:.02in solid #999}
|
||||
.tag{position:absolute;right:${inch(Math.max(0.15, b * 0.1))};top:${inch(Math.max(0.1, b * 0.08))};font:600 ${inch(Math.max(0.12, b * 0.035))} sans-serif;color:#aaa}`
|
||||
if (plan.direction === 'horizontal') {
|
||||
css += `
|
||||
.glue.glue-h{top:0;height:100%;width:auto;border-bottom:0;border-right:.02in dashed #b00;writing-mode:vertical-rl}
|
||||
.trim.trim-h{left:auto;right:0;top:0;bottom:auto;width:0;height:100%;border-bottom:0;border-right:.02in solid #999}`
|
||||
}
|
||||
if (plan.joinFill === 'bleed') {
|
||||
css += `
|
||||
.glue-guide{position:absolute;left:0;top:0;width:100%;height:${inch(plan.joinIn)};border-bottom:.01in dashed #b00;pointer-events:none}
|
||||
.glue-guide.glue-h{width:${inch(plan.joinIn)};height:100%;border-bottom:0;border-right:.01in dashed #b00}
|
||||
.glue-guide span{position:absolute;left:${inch(0.15)};top:${inch(0.1)};font:600 ${inch(Math.max(0.12, j * 0.02))} sans-serif;color:#b00;background:rgba(255,255,255,.85);padding:0 .06in;letter-spacing:.04em;text-transform:uppercase}`
|
||||
}
|
||||
return css
|
||||
}
|
||||
|
|
@ -260,7 +260,7 @@ export function createAppI18n() {
|
|||
// dir on .profile-loader from effectiveLocale (0f6d5049), so the plugin mirrors correctly
|
||||
// whether or not it owns the document. This is only about the page around it.
|
||||
//
|
||||
// LANG TRAVELS WITH DIR (rega, research.nuxtbase.app/?lang=ar): an owned page flipped to dir=rtl but
|
||||
// LANG TRAVELS WITH DIR (measured on a research page with ?lang=ar): an owned page flipped to dir=rtl but
|
||||
// kept the generator's <html lang="en">, so a screen reader read the Arabic with an English voice (and
|
||||
// ?lang=fr left lang="en" too). The page is ours, so its lang is the locale in force.
|
||||
try {
|
||||
|
|
|
|||
4
library/i18n/locales/ar/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "للهند قسمها الخاص في المقدّمة: عدد الشعوب غير المُبشَّرة فيها {count}، وهو أكثر بكثير من أي بلد آخر، لذا لكل ولاية وإقليم اتحادي فيها ملصقه الخاص.",
|
||||
"indiaStateNoGroups": "لا توجد هنا شعوب غير مُبشَّرة مُدرَجة."
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "هذا الملصق أكبر من ذاكرة هذا الجهاز. اختر حجمًا أصغر أو دقة أقل.",
|
||||
"error_failed": "تعذّر إنشاء الملصق. حاول مرة أخرى أو اختر حجمًا أصغر.",
|
||||
"optional": "(اختياري)",
|
||||
"phoneLimit": "على الهاتف، أكبر المقاسات غير متاحة: لا يستطيع الهاتف رسم ملصق بهذا الحجم. استخدم حاسوبًا لها."
|
||||
"phoneLimit": "على الهاتف، أكبر المقاسات غير متاحة: لا يستطيع الهاتف رسم ملصق بهذا الحجم. استخدم حاسوبًا لها.",
|
||||
"glue_here": "الصق هنا"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/ar/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "عرض التفاصيل كاملة",
|
||||
"showFullDetailsAria": "عرض التفاصيل كاملة لـ {label}",
|
||||
"tab": {
|
||||
"styles": "الأنماط",
|
||||
"sizes": "المقاسات"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "أنماط الملصقات",
|
||||
"intro": "كل قوالب الملصقات في مكان واحد، مرسومة من المكتبة ببيانات نموذجية. تحمل كل بطاقة اسم إعدادها، لتطلب من الوكيل هذا القالب بعينه."
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "مقاسات الملصقات",
|
||||
"waiting": "سيظهر سُلّم المقاسات (الورق القياسي، و3 × 3 أقدام، و3 × 5 أقدام، و5.5 × 5.5 أقدام، و5 × 8 أقدام) هنا بعد نشر جدول المقاسات المشترك. سيُعرض كل مقاس كمعاينة طباعة، بالاتجاه الطولي فقط."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "صفحات الخرائط",
|
||||
"packet": "حزمة الصلاة",
|
||||
"cards": "بطاقات الصلاة",
|
||||
"wall": "ألواح الجدار",
|
||||
"mapPrint": "طباعة الخريطة"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "صفحة خريطة الدولة",
|
||||
"mapSheetPacketPage": "صفحة الخريطة في الحزمة",
|
||||
"packetFull": "حزمة الصلاة، كاملة",
|
||||
"packetLightweight": "حزمة الصلاة، مختصرة",
|
||||
"cards": "بطاقات الصلاة",
|
||||
"blessPanels": "ألواح جدار البركة",
|
||||
"mapPrint": "طباعة الخريطة"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "لا يرسم المعرض الحزمة بعد. تذكر التفاصيل مولّدها ونسختها.",
|
||||
"cards": "لا يرسم المعرض البطاقات بعد. تذكر التفاصيل المولّد ومقاس البطاقة.",
|
||||
"bless": "محرّك هذا القالب ليس في المكتبة المشتركة بعد، لذا لا يستطيع المعرض رسمه.",
|
||||
"mapPrint": "يطبع هذا القالب الخريطة الحية التي ينظر إليها القارئ، لذا ليست له صورة نموذجية."
|
||||
},
|
||||
"details": {
|
||||
"configName": "اسم الإعداد",
|
||||
"copyName": "نسخ",
|
||||
"size": "المقاس",
|
||||
"fileSize": "حجم الملف",
|
||||
"fileSizeUnknown": "يُقاس عند رسم الملصق",
|
||||
"settings": "الإعدادات",
|
||||
"sample": "بيانات نموذجية",
|
||||
"renderer": "المولّد",
|
||||
"legacy": "مقاس قديم: ما زال يُطبع، لكن أداة اختيار الطباعة لم تعد تعرضه.",
|
||||
"forAgents": "لاستخدام هذا القالب، قل للوكيل: استخدم الإعداد {name}."
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/de/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "Indien hat einen eigenen Abschnitt, ganz oben: Mit {count} unerreichten Volksgruppen, weit mehr als jedes andere Land, hat jeder seiner Bundesstaaten und Unionsterritorien ein eigenes Plakat.",
|
||||
"indiaStateNoGroups": "Hier sind keine unerreichten Volksgruppen verzeichnet."
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "Dieses Poster ist zu groß für den Speicher dieses Geräts. Wählen Sie eine kleinere Größe oder eine niedrigere Auflösung.",
|
||||
"error_failed": "Das Poster konnte nicht erstellt werden. Versuchen Sie es erneut oder wählen Sie eine kleinere Größe.",
|
||||
"optional": "(optional)",
|
||||
"phoneLimit": "Auf dem Handy sind die größten Formate ausgeschaltet: Ein Handy kann kein so großes Poster zeichnen. Nutzen Sie dafür einen Computer."
|
||||
"phoneLimit": "Auf dem Handy sind die größten Formate ausgeschaltet: Ein Handy kann kein so großes Poster zeichnen. Nutzen Sie dafür einen Computer.",
|
||||
"glue_here": "hier kleben"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/de/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "Alle Details anzeigen",
|
||||
"showFullDetailsAria": "Alle Details zu {label} anzeigen",
|
||||
"tab": {
|
||||
"styles": "Stile",
|
||||
"sizes": "Größen"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "Posterstile",
|
||||
"intro": "Alle Postervorlagen an einem Ort, aus der Bibliothek mit Beispieldaten gezeichnet. Jede Karte trägt den Namen ihrer Konfiguration, damit Sie einen Agenten um genau diese bitten können."
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "Postergrößen",
|
||||
"waiting": "Die Größenstufen (Standardpapier, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft und 5 × 8 ft) erscheinen hier, sobald die gemeinsame Größentabelle veröffentlicht ist. Jede Größe wird als Druckvorschau gezeigt, nur im Hochformat."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Kartenblätter",
|
||||
"packet": "Gebetsmappe",
|
||||
"cards": "Gebetskarten",
|
||||
"wall": "Wandtafeln",
|
||||
"mapPrint": "Kartendruck"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "Länder-Kartenblatt",
|
||||
"mapSheetPacketPage": "Kartenseite der Mappe",
|
||||
"packetFull": "Gebetsmappe, vollständig",
|
||||
"packetLightweight": "Gebetsmappe, kompakt",
|
||||
"cards": "Gebetskarten",
|
||||
"blessPanels": "Tafeln der Segnen-Wand",
|
||||
"mapPrint": "Kartendruck"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "Die Galerie zeichnet die Mappe noch nicht. Die Details nennen ihren Generator und ihre Variante.",
|
||||
"cards": "Die Galerie zeichnet die Karten noch nicht. Die Details nennen den Generator und das Kartenformat.",
|
||||
"bless": "Die Engine dieser Vorlage ist noch nicht in der gemeinsamen Bibliothek, deshalb kann die Galerie sie nicht zeichnen.",
|
||||
"mapPrint": "Diese Vorlage druckt die Live-Karte, die der Leser gerade ansieht, und hat deshalb kein Beispielbild."
|
||||
},
|
||||
"details": {
|
||||
"configName": "Name der Konfiguration",
|
||||
"copyName": "Kopieren",
|
||||
"size": "Größe",
|
||||
"fileSize": "Dateigröße",
|
||||
"fileSizeUnknown": "Wird gemessen, wenn das Poster gezeichnet wird",
|
||||
"settings": "Einstellungen",
|
||||
"sample": "Beispieldaten",
|
||||
"renderer": "Generator",
|
||||
"legacy": "Alte Größe: Sie wird noch gedruckt, aber die Druckauswahl bietet sie nicht mehr an.",
|
||||
"forAgents": "Um diese Vorlage zu verwenden, sagen Sie einem Agenten: verwende die Konfiguration {name}."
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/en/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "India has its own section, first: it has {count} unreached people groups, far more than any other country, so each of its states and union territories has its own poster.",
|
||||
"indiaStateNoGroups": "No unreached people groups are listed here."
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "This poster is too large for this device's memory. Choose a smaller size or a lower resolution.",
|
||||
"error_failed": "The poster could not be rendered. Try again, or choose a smaller size.",
|
||||
"optional": "(optional)",
|
||||
"phoneLimit": "On a phone, the largest sizes are off: a phone can't draw a poster that big. Use a computer for them."
|
||||
"phoneLimit": "On a phone, the largest sizes are off: a phone can't draw a poster that big. Use a computer for them.",
|
||||
"glue_here": "glue here"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/en/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "Show full details",
|
||||
"showFullDetailsAria": "Show full details for {label}",
|
||||
"tab": {
|
||||
"styles": "Styles",
|
||||
"sizes": "Sizes"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "Poster styles",
|
||||
"intro": "Every poster template in one place, drawn from the library with sample data. Each card is named by its configuration, so you can ask an agent for exactly that one."
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "Poster sizes",
|
||||
"waiting": "The size ladder (standard paper, 3 × 3 ft, 3 × 5 ft, 5.5 × 5.5 ft and 5 × 8 ft) appears here once the shared size table is published. Each size will be shown as a print preview, portrait only."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Map sheets",
|
||||
"packet": "Prayer packet",
|
||||
"cards": "Prayer cards",
|
||||
"wall": "Wall panels",
|
||||
"mapPrint": "Map print"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "Country map sheet",
|
||||
"mapSheetPacketPage": "Packet map page",
|
||||
"packetFull": "Prayer packet, full",
|
||||
"packetLightweight": "Prayer packet, lightweight",
|
||||
"cards": "Prayer cards",
|
||||
"blessPanels": "Bless wall panels",
|
||||
"mapPrint": "Map print"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "The gallery does not draw the packet yet. The details name its renderer and variant.",
|
||||
"cards": "The gallery does not draw the cards yet. The details name the renderer and the card stock.",
|
||||
"bless": "This template's engine is not in the shared library yet, so the gallery cannot draw it.",
|
||||
"mapPrint": "This template prints the live map the reader is looking at, so it has no sample picture."
|
||||
},
|
||||
"details": {
|
||||
"configName": "Configuration name",
|
||||
"copyName": "Copy",
|
||||
"size": "Size",
|
||||
"fileSize": "File size",
|
||||
"fileSizeUnknown": "Measured when the poster is drawn",
|
||||
"settings": "Settings",
|
||||
"sample": "Sample data",
|
||||
"renderer": "Renderer",
|
||||
"legacy": "Legacy size: it still prints, but the print picker no longer offers it.",
|
||||
"forAgents": "To use this template, tell an agent: use configuration {name}."
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/es/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "India tiene su propia sección, la primera: tiene {count} pueblos no alcanzados, muchos más que cualquier otro país, así que cada uno de sus estados y territorios de la unión tiene su propio cartel.",
|
||||
"indiaStateNoGroups": "Aquí no figura ningún pueblo no alcanzado."
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "Este póster es demasiado grande para la memoria de este dispositivo. Elige un tamaño menor o una resolución más baja.",
|
||||
"error_failed": "No se pudo generar el póster. Inténtalo de nuevo o elige un tamaño menor.",
|
||||
"optional": "(opcional)",
|
||||
"phoneLimit": "En un teléfono, los tamaños más grandes están desactivados: un teléfono no puede dibujar un póster tan grande. Usa un ordenador para ellos."
|
||||
"phoneLimit": "En un teléfono, los tamaños más grandes están desactivados: un teléfono no puede dibujar un póster tan grande. Usa un ordenador para ellos.",
|
||||
"glue_here": "pegar aquí"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/es/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "Ver todos los detalles",
|
||||
"showFullDetailsAria": "Ver todos los detalles de {label}",
|
||||
"tab": {
|
||||
"styles": "Estilos",
|
||||
"sizes": "Tamaños"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "Estilos de póster",
|
||||
"intro": "Todas las plantillas de póster en un solo lugar, dibujadas desde la biblioteca con datos de ejemplo. Cada tarjeta lleva el nombre de su configuración, para que puedas pedirle a un agente exactamente esa."
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "Tamaños de póster",
|
||||
"waiting": "La escala de tamaños (papel estándar, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft y 5 × 8 ft) aparecerá aquí cuando se publique la tabla de tamaños compartida. Cada tamaño se mostrará como vista previa de impresión, solo en vertical."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Hojas de mapa",
|
||||
"packet": "Paquete de oración",
|
||||
"cards": "Tarjetas de oración",
|
||||
"wall": "Paneles de pared",
|
||||
"mapPrint": "Impresión del mapa"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "Hoja de mapa del país",
|
||||
"mapSheetPacketPage": "Página de mapa del paquete",
|
||||
"packetFull": "Paquete de oración, completo",
|
||||
"packetLightweight": "Paquete de oración, ligero",
|
||||
"cards": "Tarjetas de oración",
|
||||
"blessPanels": "Paneles del muro Bendecir",
|
||||
"mapPrint": "Impresión del mapa"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "La galería todavía no dibuja el paquete. Los detalles indican su generador y su variante.",
|
||||
"cards": "La galería todavía no dibuja las tarjetas. Los detalles indican el generador y el tipo de tarjeta.",
|
||||
"bless": "El motor de esta plantilla aún no está en la biblioteca compartida, así que la galería no puede dibujarla.",
|
||||
"mapPrint": "Esta plantilla imprime el mapa en vivo que está mirando el lector, así que no tiene imagen de ejemplo."
|
||||
},
|
||||
"details": {
|
||||
"configName": "Nombre de la configuración",
|
||||
"copyName": "Copiar",
|
||||
"size": "Tamaño",
|
||||
"fileSize": "Tamaño del archivo",
|
||||
"fileSizeUnknown": "Se mide al dibujar el póster",
|
||||
"settings": "Ajustes",
|
||||
"sample": "Datos de ejemplo",
|
||||
"renderer": "Generador",
|
||||
"legacy": "Tamaño heredado: todavía se imprime, pero el selector de impresión ya no lo ofrece.",
|
||||
"forAgents": "Para usar esta plantilla, dile a un agente: usa la configuración {name}."
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/fr/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "L’Inde a sa propre section, en premier : elle compte {count} peuples non atteints, bien plus que tout autre pays ; chacun de ses États et territoires de l’Union a donc sa propre affiche.",
|
||||
"indiaStateNoGroups": "Aucun peuple non atteint n’est répertorié ici."
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "Cette affiche est trop grande pour la mémoire de cet appareil. Choisissez un format plus petit ou une résolution plus basse.",
|
||||
"error_failed": "L’affiche n’a pas pu être générée. Réessayez ou choisissez un format plus petit.",
|
||||
"optional": "(facultatif)",
|
||||
"phoneLimit": "Sur un téléphone, les plus grands formats sont désactivés : un téléphone ne peut pas dessiner une affiche aussi grande. Utilisez un ordinateur pour ceux-là."
|
||||
"phoneLimit": "Sur un téléphone, les plus grands formats sont désactivés : un téléphone ne peut pas dessiner une affiche aussi grande. Utilisez un ordinateur pour ceux-là.",
|
||||
"glue_here": "coller ici"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/fr/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "Afficher tous les détails",
|
||||
"showFullDetailsAria": "Afficher tous les détails de {label}",
|
||||
"tab": {
|
||||
"styles": "Styles",
|
||||
"sizes": "Formats"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "Styles d'affiche",
|
||||
"intro": "Tous les modèles d'affiche au même endroit, dessinés depuis la bibliothèque avec des données d'exemple. Chaque carte porte le nom de sa configuration, pour que vous puissiez demander exactement celle-ci à un agent."
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "Formats d'affiche",
|
||||
"waiting": "L'échelle des formats (papier standard, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft et 5 × 8 ft) apparaîtra ici dès que la table des formats partagée sera publiée. Chaque format sera présenté en aperçu avant impression, en portrait uniquement."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Feuilles de carte",
|
||||
"packet": "Dossier de prière",
|
||||
"cards": "Cartes de prière",
|
||||
"wall": "Panneaux muraux",
|
||||
"mapPrint": "Impression de la carte"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "Feuille de carte du pays",
|
||||
"mapSheetPacketPage": "Page de carte du dossier",
|
||||
"packetFull": "Dossier de prière, complet",
|
||||
"packetLightweight": "Dossier de prière, allégé",
|
||||
"cards": "Cartes de prière",
|
||||
"blessPanels": "Panneaux du mur Bénir",
|
||||
"mapPrint": "Impression de la carte"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "La galerie ne dessine pas encore le dossier. Les détails indiquent son générateur et sa variante.",
|
||||
"cards": "La galerie ne dessine pas encore les cartes. Les détails indiquent le générateur et le format de carte.",
|
||||
"bless": "Le moteur de ce modèle n'est pas encore dans la bibliothèque partagée, la galerie ne peut donc pas le dessiner.",
|
||||
"mapPrint": "Ce modèle imprime la carte en direct que le lecteur regarde, il n'a donc pas d'image d'exemple."
|
||||
},
|
||||
"details": {
|
||||
"configName": "Nom de la configuration",
|
||||
"copyName": "Copier",
|
||||
"size": "Format",
|
||||
"fileSize": "Taille du fichier",
|
||||
"fileSizeUnknown": "Mesurée quand l'affiche est dessinée",
|
||||
"settings": "Réglages",
|
||||
"sample": "Données d'exemple",
|
||||
"renderer": "Générateur",
|
||||
"legacy": "Format ancien : il s'imprime encore, mais le sélecteur d'impression ne le propose plus.",
|
||||
"forAgents": "Pour utiliser ce modèle, dites à un agent : utilise la configuration {name}."
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/hi/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "भारत का अपना अलग खंड है, सबसे पहले: इसमें {count} अपहुँचे जनसमूह हैं, किसी भी अन्य देश से कहीं अधिक, इसलिए इसके हर राज्य और केंद्र शासित प्रदेश का अपना पोस्टर है।",
|
||||
"indiaStateNoGroups": "यहाँ कोई अपहुँचा जनसमूह सूचीबद्ध नहीं है।"
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "यह पोस्टर इस डिवाइस की मेमोरी के लिए बहुत बड़ा है। छोटा आकार या कम रिज़ॉल्यूशन चुनें।",
|
||||
"error_failed": "पोस्टर नहीं बन सका। फिर से कोशिश करें या छोटा आकार चुनें।",
|
||||
"optional": "(वैकल्पिक)",
|
||||
"phoneLimit": "फ़ोन पर सबसे बड़े आकार बंद हैं: फ़ोन इतना बड़ा पोस्टर नहीं बना सकता। उनके लिए कंप्यूटर का उपयोग करें।"
|
||||
"phoneLimit": "फ़ोन पर सबसे बड़े आकार बंद हैं: फ़ोन इतना बड़ा पोस्टर नहीं बना सकता। उनके लिए कंप्यूटर का उपयोग करें।",
|
||||
"glue_here": "यहाँ चिपकाएँ"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/hi/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "पूरा विवरण दिखाएँ",
|
||||
"showFullDetailsAria": "{label} का पूरा विवरण दिखाएँ",
|
||||
"tab": {
|
||||
"styles": "शैलियाँ",
|
||||
"sizes": "आकार"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "पोस्टर शैलियाँ",
|
||||
"intro": "सभी पोस्टर टेम्पलेट एक ही जगह, लाइब्रेरी से नमूना डेटा के साथ बनाए गए। हर कार्ड पर उसके कॉन्फ़िगरेशन का नाम है, ताकि आप किसी एजेंट से ठीक वही माँग सकें।"
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "पोस्टर आकार",
|
||||
"waiting": "साझा आकार तालिका प्रकाशित होते ही आकारों की सीढ़ी (मानक कागज़, 3 × 3 फ़ुट, 3 × 5 फ़ुट, 5.5 × 5.5 फ़ुट और 5 × 8 फ़ुट) यहाँ दिखेगी। हर आकार प्रिंट पूर्वावलोकन के रूप में दिखेगा, केवल खड़े (पोर्ट्रेट) रूप में।"
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "मानचित्र पृष्ठ",
|
||||
"packet": "प्रार्थना पैकेट",
|
||||
"cards": "प्रार्थना कार्ड",
|
||||
"wall": "दीवार पैनल",
|
||||
"mapPrint": "मानचित्र प्रिंट"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "देश का मानचित्र पृष्ठ",
|
||||
"mapSheetPacketPage": "पैकेट का मानचित्र पृष्ठ",
|
||||
"packetFull": "प्रार्थना पैकेट, पूरा",
|
||||
"packetLightweight": "प्रार्थना पैकेट, हल्का",
|
||||
"cards": "प्रार्थना कार्ड",
|
||||
"blessPanels": "आशीष दीवार के पैनल",
|
||||
"mapPrint": "मानचित्र प्रिंट"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "गैलरी अभी पैकेट नहीं बनाती। विवरण में उसका जनरेटर और संस्करण बताया गया है।",
|
||||
"cards": "गैलरी अभी कार्ड नहीं बनाती। विवरण में जनरेटर और कार्ड का आकार बताया गया है।",
|
||||
"bless": "इस टेम्पलेट का इंजन अभी साझा लाइब्रेरी में नहीं है, इसलिए गैलरी इसे नहीं बना सकती।",
|
||||
"mapPrint": "यह टेम्पलेट वह लाइव मानचित्र प्रिंट करता है जिसे पाठक देख रहा है, इसलिए इसकी कोई नमूना तस्वीर नहीं है।"
|
||||
},
|
||||
"details": {
|
||||
"configName": "कॉन्फ़िगरेशन का नाम",
|
||||
"copyName": "कॉपी करें",
|
||||
"size": "आकार",
|
||||
"fileSize": "फ़ाइल का आकार",
|
||||
"fileSizeUnknown": "पोस्टर बनने पर मापा जाता है",
|
||||
"settings": "सेटिंग्स",
|
||||
"sample": "नमूना डेटा",
|
||||
"renderer": "जनरेटर",
|
||||
"legacy": "पुराना आकार: यह अब भी प्रिंट होता है, पर प्रिंट चयनकर्ता अब इसे नहीं दिखाता।",
|
||||
"forAgents": "यह टेम्पलेट इस्तेमाल करने के लिए किसी एजेंट से कहें: कॉन्फ़िगरेशन {name} इस्तेमाल करो।"
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/it/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "L’India ha una sezione tutta sua, la prima: conta {count} popoli non raggiunti, molti più di qualsiasi altro paese, quindi ognuno dei suoi stati e territori dell’Unione ha il proprio manifesto.",
|
||||
"indiaStateNoGroups": "Qui non è elencato alcun popolo non raggiunto."
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "Questo poster è troppo grande per la memoria di questo dispositivo. Scegli un formato più piccolo o una risoluzione più bassa.",
|
||||
"error_failed": "Impossibile generare il poster. Riprova o scegli un formato più piccolo.",
|
||||
"optional": "(facoltativo)",
|
||||
"phoneLimit": "Sul telefono i formati più grandi sono disattivati: un telefono non può disegnare un poster così grande. Per quelli usa un computer."
|
||||
"phoneLimit": "Sul telefono i formati più grandi sono disattivati: un telefono non può disegnare un poster così grande. Per quelli usa un computer.",
|
||||
"glue_here": "incollare qui"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/it/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "Mostra tutti i dettagli",
|
||||
"showFullDetailsAria": "Mostra tutti i dettagli di {label}",
|
||||
"tab": {
|
||||
"styles": "Stili",
|
||||
"sizes": "Formati"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "Stili dei poster",
|
||||
"intro": "Tutti i modelli di poster in un unico posto, disegnati dalla libreria con dati di esempio. Ogni scheda porta il nome della sua configurazione, così puoi chiedere a un agente esattamente quella."
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "Formati dei poster",
|
||||
"waiting": "La scala dei formati (carta standard, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft e 5 × 8 ft) comparirà qui quando sarà pubblicata la tabella dei formati condivisa. Ogni formato sarà mostrato come anteprima di stampa, solo in verticale."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Fogli mappa",
|
||||
"packet": "Fascicolo di preghiera",
|
||||
"cards": "Schede di preghiera",
|
||||
"wall": "Pannelli da parete",
|
||||
"mapPrint": "Stampa della mappa"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "Foglio mappa del paese",
|
||||
"mapSheetPacketPage": "Pagina mappa del fascicolo",
|
||||
"packetFull": "Fascicolo di preghiera, completo",
|
||||
"packetLightweight": "Fascicolo di preghiera, leggero",
|
||||
"cards": "Schede di preghiera",
|
||||
"blessPanels": "Pannelli del muro Benedici",
|
||||
"mapPrint": "Stampa della mappa"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "La galleria non disegna ancora il fascicolo. I dettagli indicano il suo generatore e la variante.",
|
||||
"cards": "La galleria non disegna ancora le schede. I dettagli indicano il generatore e il formato della scheda.",
|
||||
"bless": "Il motore di questo modello non è ancora nella libreria condivisa, quindi la galleria non può disegnarlo.",
|
||||
"mapPrint": "Questo modello stampa la mappa dal vivo che il lettore sta guardando, quindi non ha un'immagine di esempio."
|
||||
},
|
||||
"details": {
|
||||
"configName": "Nome della configurazione",
|
||||
"copyName": "Copia",
|
||||
"size": "Formato",
|
||||
"fileSize": "Dimensione del file",
|
||||
"fileSizeUnknown": "Misurata quando il poster viene disegnato",
|
||||
"settings": "Impostazioni",
|
||||
"sample": "Dati di esempio",
|
||||
"renderer": "Generatore",
|
||||
"legacy": "Formato precedente: si stampa ancora, ma il selettore di stampa non lo offre più.",
|
||||
"forAgents": "Per usare questo modello, di' a un agente: usa la configurazione {name}."
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/pt/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "A Índia tem a sua própria seção, a primeira: tem {count} povos não alcançados, muito mais do que qualquer outro país, por isso cada um dos seus estados e territórios da união tem o seu próprio cartaz.",
|
||||
"indiaStateNoGroups": "Nenhum povo não alcançado está listado aqui."
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "Este pôster é grande demais para a memória deste dispositivo. Escolha um tamanho menor ou uma resolução mais baixa.",
|
||||
"error_failed": "Não foi possível gerar o pôster. Tente novamente ou escolha um tamanho menor.",
|
||||
"optional": "(opcional)",
|
||||
"phoneLimit": "No telefone, os tamanhos maiores ficam desativados: um telefone não consegue desenhar um cartaz tão grande. Use um computador para eles."
|
||||
"phoneLimit": "No telefone, os tamanhos maiores ficam desativados: um telefone não consegue desenhar um cartaz tão grande. Use um computador para eles.",
|
||||
"glue_here": "colar aqui"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/pt/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "Mostrar todos os detalhes",
|
||||
"showFullDetailsAria": "Mostrar todos os detalhes de {label}",
|
||||
"tab": {
|
||||
"styles": "Estilos",
|
||||
"sizes": "Tamanhos"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "Estilos de pôster",
|
||||
"intro": "Todos os modelos de pôster num só lugar, desenhados a partir da biblioteca com dados de exemplo. Cada cartão tem o nome da sua configuração, para você pedir exatamente essa a um agente."
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "Tamanhos de pôster",
|
||||
"waiting": "A escala de tamanhos (papel padrão, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft e 5 × 8 ft) aparecerá aqui quando a tabela de tamanhos compartilhada for publicada. Cada tamanho será mostrado como pré-visualização de impressão, apenas na vertical."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Folhas de mapa",
|
||||
"packet": "Pacote de oração",
|
||||
"cards": "Cartões de oração",
|
||||
"wall": "Painéis de parede",
|
||||
"mapPrint": "Impressão do mapa"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "Folha de mapa do país",
|
||||
"mapSheetPacketPage": "Página de mapa do pacote",
|
||||
"packetFull": "Pacote de oração, completo",
|
||||
"packetLightweight": "Pacote de oração, leve",
|
||||
"cards": "Cartões de oração",
|
||||
"blessPanels": "Painéis do mural Abençoar",
|
||||
"mapPrint": "Impressão do mapa"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "A galeria ainda não desenha o pacote. Os detalhes indicam o seu gerador e a variante.",
|
||||
"cards": "A galeria ainda não desenha os cartões. Os detalhes indicam o gerador e o formato do cartão.",
|
||||
"bless": "O motor deste modelo ainda não está na biblioteca compartilhada, por isso a galeria não consegue desenhá-lo.",
|
||||
"mapPrint": "Este modelo imprime o mapa ao vivo que o leitor está vendo, por isso não tem imagem de exemplo."
|
||||
},
|
||||
"details": {
|
||||
"configName": "Nome da configuração",
|
||||
"copyName": "Copiar",
|
||||
"size": "Tamanho",
|
||||
"fileSize": "Tamanho do arquivo",
|
||||
"fileSizeUnknown": "Medido quando o pôster é desenhado",
|
||||
"settings": "Configurações",
|
||||
"sample": "Dados de exemplo",
|
||||
"renderer": "Gerador",
|
||||
"legacy": "Tamanho antigo: ainda imprime, mas o seletor de impressão já não o oferece.",
|
||||
"forAgents": "Para usar este modelo, diga a um agente: use a configuração {name}."
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/ro/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "India are propria secțiune, prima: are {count} popoare neatinse, mult mai multe decât orice altă țară, așa că fiecare dintre statele și teritoriile sale ale Uniunii are propriul afiș.",
|
||||
"indiaStateNoGroups": "Aici nu este listat niciun popor neatins."
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "Acest poster este prea mare pentru memoria acestui dispozitiv. Alegeți o dimensiune mai mică sau o rezoluție mai mică.",
|
||||
"error_failed": "Posterul nu a putut fi generat. Încercați din nou sau alegeți o dimensiune mai mică.",
|
||||
"optional": "(opțional)",
|
||||
"phoneLimit": "Pe telefon, cele mai mari dimensiuni sunt dezactivate: un telefon nu poate desena un poster atât de mare. Folosește un calculator pentru ele."
|
||||
"phoneLimit": "Pe telefon, cele mai mari dimensiuni sunt dezactivate: un telefon nu poate desena un poster atât de mare. Folosește un calculator pentru ele.",
|
||||
"glue_here": "lipiți aici"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/ro/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "Arată toate detaliile",
|
||||
"showFullDetailsAria": "Arată toate detaliile pentru {label}",
|
||||
"tab": {
|
||||
"styles": "Stiluri",
|
||||
"sizes": "Dimensiuni"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "Stiluri de afiș",
|
||||
"intro": "Toate șabloanele de afiș într-un singur loc, desenate din bibliotecă cu date de exemplu. Fiecare card poartă numele configurației sale, ca să poți cere unui agent exact acel șablon."
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "Dimensiuni de afiș",
|
||||
"waiting": "Scara dimensiunilor (hârtie standard, 3 × 3 ft, 3 × 5 ft, 5,5 × 5,5 ft și 5 × 8 ft) va apărea aici după publicarea tabelului comun de dimensiuni. Fiecare dimensiune va fi afișată ca previzualizare de tipărire, doar pe verticală."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Foi de hartă",
|
||||
"packet": "Pachet de rugăciune",
|
||||
"cards": "Cartonașe de rugăciune",
|
||||
"wall": "Panouri de perete",
|
||||
"mapPrint": "Tipărirea hărții"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "Foaie de hartă a țării",
|
||||
"mapSheetPacketPage": "Pagina de hartă a pachetului",
|
||||
"packetFull": "Pachet de rugăciune, complet",
|
||||
"packetLightweight": "Pachet de rugăciune, ușor",
|
||||
"cards": "Cartonașe de rugăciune",
|
||||
"blessPanels": "Panourile peretelui Binecuvântează",
|
||||
"mapPrint": "Tipărirea hărții"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "Galeria nu desenează încă pachetul. Detaliile numesc generatorul și varianta lui.",
|
||||
"cards": "Galeria nu desenează încă cartonașele. Detaliile numesc generatorul și formatul cartonașului.",
|
||||
"bless": "Motorul acestui șablon nu este încă în biblioteca comună, așa că galeria nu îl poate desena.",
|
||||
"mapPrint": "Acest șablon tipărește harta live pe care o privește cititorul, așa că nu are imagine de exemplu."
|
||||
},
|
||||
"details": {
|
||||
"configName": "Numele configurației",
|
||||
"copyName": "Copiază",
|
||||
"size": "Dimensiune",
|
||||
"fileSize": "Dimensiunea fișierului",
|
||||
"fileSizeUnknown": "Se măsoară când afișul este desenat",
|
||||
"settings": "Setări",
|
||||
"sample": "Date de exemplu",
|
||||
"renderer": "Generator",
|
||||
"legacy": "Dimensiune veche: încă se tipărește, dar selectorul de tipărire nu o mai oferă.",
|
||||
"forAgents": "Ca să folosești acest șablon, spune-i unui agent: folosește configurația {name}."
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/ru/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "У Индии свой раздел, первый: число недостигнутых народов в ней — {count}, намного больше, чем в любой другой стране, поэтому у каждого её штата и союзной территории есть свой плакат.",
|
||||
"indiaStateNoGroups": "Здесь не указано ни одного недостигнутого народа."
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "Этот плакат слишком велик для памяти устройства. Выберите меньший размер или более низкое разрешение.",
|
||||
"error_failed": "Не удалось создать плакат. Попробуйте ещё раз или выберите меньший размер.",
|
||||
"optional": "(необязательно)",
|
||||
"phoneLimit": "На телефоне самые большие размеры отключены: телефон не может нарисовать такой большой плакат. Используйте для них компьютер."
|
||||
"phoneLimit": "На телефоне самые большие размеры отключены: телефон не может нарисовать такой большой плакат. Используйте для них компьютер.",
|
||||
"glue_here": "клеить здесь"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/ru/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "Показать все сведения",
|
||||
"showFullDetailsAria": "Показать все сведения: {label}",
|
||||
"tab": {
|
||||
"styles": "Стили",
|
||||
"sizes": "Размеры"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "Стили плакатов",
|
||||
"intro": "Все шаблоны плакатов в одном месте, нарисованные из библиотеки на примерных данных. Каждая карточка названа по своей конфигурации, чтобы можно было попросить агента именно о ней."
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "Размеры плакатов",
|
||||
"waiting": "Шкала размеров (стандартная бумага, 3 × 3 фута, 3 × 5 футов, 5,5 × 5,5 фута и 5 × 8 футов) появится здесь, когда будет опубликована общая таблица размеров. Каждый размер будет показан как предпросмотр печати, только в книжной ориентации."
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "Листы карт",
|
||||
"packet": "Молитвенный пакет",
|
||||
"cards": "Молитвенные карточки",
|
||||
"wall": "Настенные панели",
|
||||
"mapPrint": "Печать карты"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "Лист карты страны",
|
||||
"mapSheetPacketPage": "Страница карты в пакете",
|
||||
"packetFull": "Молитвенный пакет, полный",
|
||||
"packetLightweight": "Молитвенный пакет, облегчённый",
|
||||
"cards": "Молитвенные карточки",
|
||||
"blessPanels": "Панели стены «Благословить»",
|
||||
"mapPrint": "Печать карты"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "Галерея пока не рисует пакет. В сведениях указаны его генератор и вариант.",
|
||||
"cards": "Галерея пока не рисует карточки. В сведениях указаны генератор и формат карточки.",
|
||||
"bless": "Движка этого шаблона пока нет в общей библиотеке, поэтому галерея не может его нарисовать.",
|
||||
"mapPrint": "Этот шаблон печатает живую карту, на которую смотрит читатель, поэтому у него нет примерного изображения."
|
||||
},
|
||||
"details": {
|
||||
"configName": "Название конфигурации",
|
||||
"copyName": "Копировать",
|
||||
"size": "Размер",
|
||||
"fileSize": "Размер файла",
|
||||
"fileSizeUnknown": "Измеряется, когда плакат нарисован",
|
||||
"settings": "Параметры",
|
||||
"sample": "Примерные данные",
|
||||
"renderer": "Генератор",
|
||||
"legacy": "Устаревший размер: он ещё печатается, но в выборе печати его больше нет.",
|
||||
"forAgents": "Чтобы использовать этот шаблон, скажите агенту: используй конфигурацию {name}."
|
||||
}
|
||||
}
|
||||
4
library/i18n/locales/zh/countryPosters.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"indiaNote": "印度有单独的一栏,排在最前:这里有 {count} 个未得之民族群,远多于任何其他国家,因此它的每个邦和中央直辖区都有自己的海报。",
|
||||
"indiaStateNoGroups": "此处未列出任何未得之民族群。"
|
||||
}
|
||||
|
|
@ -39,5 +39,6 @@
|
|||
"error_too_large": "此海报超出了本设备的内存。请选择较小的尺寸或较低的分辨率。",
|
||||
"error_failed": "无法生成海报。请重试或选择较小的尺寸。",
|
||||
"optional": "(可选)",
|
||||
"phoneLimit": "在手机上,最大的尺寸已关闭:手机无法绘制这么大的海报。请使用电脑制作这些尺寸。"
|
||||
"phoneLimit": "在手机上,最大的尺寸已关闭:手机无法绘制这么大的海报。请使用电脑制作这些尺寸。",
|
||||
"glue_here": "在此粘贴"
|
||||
}
|
||||
|
|
|
|||
50
library/i18n/locales/zh/posterDesign.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"showFullDetails": "显示完整详情",
|
||||
"showFullDetailsAria": "显示{label}的完整详情",
|
||||
"tab": {
|
||||
"styles": "样式",
|
||||
"sizes": "尺寸"
|
||||
},
|
||||
"styles": {
|
||||
"heading": "海报样式",
|
||||
"intro": "所有海报模板集中在一处,均由组件库用示例数据绘制。每张卡片都以其配置命名,方便你请代理准确使用那一种。"
|
||||
},
|
||||
"sizes": {
|
||||
"heading": "海报尺寸",
|
||||
"waiting": "共享尺寸表发布后,尺寸阶梯(标准纸张、3 × 3 英尺、3 × 5 英尺、5.5 × 5.5 英尺和 5 × 8 英尺)将显示在这里。每种尺寸都会以打印预览呈现,仅限纵向。"
|
||||
},
|
||||
"family": {
|
||||
"mapSheet": "地图页",
|
||||
"packet": "祷告资料包",
|
||||
"cards": "祷告卡",
|
||||
"wall": "墙面展板",
|
||||
"mapPrint": "地图打印"
|
||||
},
|
||||
"tpl": {
|
||||
"mapSheetCountry": "国家地图页",
|
||||
"mapSheetPacketPage": "资料包地图页",
|
||||
"packetFull": "祷告资料包(完整版)",
|
||||
"packetLightweight": "祷告资料包(精简版)",
|
||||
"cards": "祷告卡",
|
||||
"blessPanels": "祝福墙展板",
|
||||
"mapPrint": "地图打印"
|
||||
},
|
||||
"missing": {
|
||||
"packet": "图库暂不绘制资料包。详情中列出了它的生成器和版本。",
|
||||
"cards": "图库暂不绘制祷告卡。详情中列出了生成器和卡片规格。",
|
||||
"bless": "此模板的引擎尚未进入共享组件库,因此图库无法绘制。",
|
||||
"mapPrint": "此模板打印读者正在查看的实时地图,因此没有示例图片。"
|
||||
},
|
||||
"details": {
|
||||
"configName": "配置名称",
|
||||
"copyName": "复制",
|
||||
"size": "尺寸",
|
||||
"fileSize": "文件大小",
|
||||
"fileSizeUnknown": "海报绘制后测量",
|
||||
"settings": "设置",
|
||||
"sample": "示例数据",
|
||||
"renderer": "生成器",
|
||||
"legacy": "旧尺寸:仍可打印,但打印选择器已不再提供。",
|
||||
"forAgents": "要使用此模板,请告诉代理:使用配置 {name}。"
|
||||
}
|
||||
}
|
||||
|
|
@ -58,8 +58,9 @@
|
|||
* keeps the renderer's own default rather than a fabricated position.
|
||||
*
|
||||
* A null sheet is a tier too: India (sheet null, too many groups for any paper size) is its own
|
||||
* 1-of-1 series. Skipping it printed "map 1 of 0" on India's packet page 1 from the Regions tab
|
||||
* while the wall's own copy printed "map 1 of 1" -- the two paths disagreeing again.
|
||||
* 1-of-1 series. Skipping it gave India's sheet "map 1 of 0" on the Regions path and "map 1 of 1" on the
|
||||
* wall's old copy. Neither was visible then (the map-only packet page prints no series line, and India's
|
||||
* poster is gated), but any India sheet with a header would have shown it.
|
||||
* Every null-sheet country shares that ONE oversized series: a second would make India "map 1 of 2".
|
||||
* check:country-sheet-one-path fails when that happens, so it is decided, not drifted into.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -143,5 +143,49 @@
|
|||
"emit-countries.mjs), so the SHAPE was replaced: not 'split the set' but 'ask which one,",
|
||||
"then give them that poster'. The numbers are gone from the headings for the same reason."
|
||||
]
|
||||
},
|
||||
"indiaState": {
|
||||
"label": "India states and union territories — one poster for each of the 34 with people groups listed",
|
||||
"dataDir": "plugin-designs/my-upg-100-list/data",
|
||||
"countryMode": true,
|
||||
"funnel": false,
|
||||
"poster": {
|
||||
"kicker": "Unreached People Groups",
|
||||
"titleQualifier": "Pray for",
|
||||
"seriesName": "India State Maps",
|
||||
"sideBoxHeading": "What is a State Prayer Map?",
|
||||
"sideBoxBody": "This map is one of a {approxLists}-map set called the {seriesName} — one sheet for each state and union territory of India, naming the unreached people groups who live there so a reader can pray for them by name."
|
||||
},
|
||||
"_poster_why": [
|
||||
"A state poster is a country poster one level down: the Country Posters wall prints one for each of India's",
|
||||
"states and union territories that has people groups listed (34 of 36; data/people-groups/india-states.json).",
|
||||
"The country series' text cannot be reused: it would call Gujarāt's poster one of the ~170 'Country Maps',",
|
||||
"one sheet for each country. The term is 'state and union territory', never 'province'. Approved as the",
|
||||
"default wording 2026-09-30 and shown as editable; a change is a strings-only follow-up."
|
||||
],
|
||||
"strings": {
|
||||
"map_page_kicker": "PRAY FOR THIS STATE",
|
||||
"map_page_sub": "A prayer list of {count} unreached people groups who live here",
|
||||
"points_intro": "One hour with God, five minutes each. Pray these twelve for the peoples of this state.",
|
||||
"christian_lede": "This is the same grid — one human life in weeks. Now each box is a share of the people in this state. The shaded boxes are the evangelical share of their population; the rest are waiting."
|
||||
},
|
||||
"_strings_why": [
|
||||
"The country series' four keys with 'country' read as 'state'. Used by the Node generator; the browser",
|
||||
"packet reads the localized packet-strings.json, which is series-invariant."
|
||||
],
|
||||
"networks": [
|
||||
[
|
||||
"1 · Disciples.",
|
||||
"Ask a disciple which state God has put on their heart, then give them that state's poster to print and prepare."
|
||||
],
|
||||
[
|
||||
"2 · Churches.",
|
||||
"List the churches you know and ask each the same question, so every church has a poster to pray over together."
|
||||
],
|
||||
[
|
||||
"3 · Indigenous leaders.",
|
||||
"Ask which leaders you know who live in one of these states, and give them their own state's map to pray for it specifically."
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
package.json
|
|
@ -251,6 +251,7 @@
|
|||
"check:window-globals-released": "node internal/check-window-globals-released.mjs",
|
||||
"check:poster-card-fits": "node internal/check-poster-card-fits.mjs",
|
||||
"check:poster-degrades": "node internal/check-poster-degrades-honestly.mjs",
|
||||
"check:poster-design-catalogue": "node plugin-designs/poster-design/scripts/check-catalogue.mjs",
|
||||
"check:poster-fetch-guard": "node internal/check-poster-fetch-guard.mjs",
|
||||
"check:poster-print-fallback": "node internal/check-poster-print-fallback.mjs",
|
||||
"check:posters-exportable": "node internal/check-posters-exportable.mjs",
|
||||
|
|
@ -347,7 +348,7 @@
|
|||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -406,7 +407,9 @@
|
|||
"check:snippets-draw": "node internal/check-snippets-draw.mjs",
|
||||
"check:snippets-draw:mapbox": "node internal/check-snippets-draw.mjs --mapbox",
|
||||
"pin:poster-render": "node library/scripts/posters/pin-poster-render.mjs",
|
||||
"check:board-map-options": "node internal/check-board-map-options.mjs"
|
||||
"check:board-map-options": "node internal/check-board-map-options.mjs",
|
||||
"check:panel-sheets": "node internal/check-panel-sheets.mjs",
|
||||
"check:poster-wall-host-seams": "node internal/check-poster-wall-host-seams.mjs"
|
||||
},
|
||||
"//scripts": {
|
||||
"_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.",
|
||||
|
|
@ -449,7 +452,7 @@
|
|||
"check:composite-token-url": "a composite embedded BARE (no outer ProfileLoader) must honour a tokenUrl-only config: it reads tokenUrl, forwards it, and asks the token service only when no token is given. The expectation is derived from ProfileLoader, not restated. Headless; in gates:pre.",
|
||||
"check:composites-mount": "every capability-4/5 composite's OWN setup() and mounted hooks run without throwing, in en and ar, headless (no browser): vite loads each composite as its bundle entry defines it (configureApp, and ProfileLoader's provides where the entry mounts ProfileLoader), and @vue/runtime-core mounts it on a no-op renderer. Exists because 08f89f55 shipped two composites calling getRuntimeLocale/detectLocale/RTL_LOCALES with no import: the build was green, setup threw a ReferenceError in every locale, and Big Picture and Simple Research rendered nothing. Proves setup runs; cannot prove the children render.",
|
||||
"check:country-names": "every ISO 3166-1 alpha-3 our data uses must resolve to a localized country name. Guards the JOIN, not Intl: library/data/iso3-to-alpha2.json drifting from library/data/countries.json makes a country render as a bare code, which still looks like a value. Checks en/ar/zh (ar is our only RTL locale; zh makes an untranslated Latin code unmistakable) and asserts BOTH directions of drift. Carries two negative arms — an unassigned code and a malformed one — and FAILS if either resolves, because a coverage count cannot otherwise tell a hit from a lookup that returns its input.",
|
||||
"check:country-sheet-one-path": "a country sheet is drawn through ONE path, @map/packet/countrySheet.js, so one country cannot print two posters. Parses (comments cannot count) every plugin source naming generateCountrySheet/makeCountrySheet: a direct generateCountrySheet in code FAILS, every makeCountrySheet call must pass `countries` (the series list), and both known callers (the Country Posters wall, regions-list) must be found. Also runs seriesFor over the real country-posters.json with named answers: India 1 of 1, every 3x3 country N of the 3x3 tier with no repeat, a code absent from the data no series.",
|
||||
"check:country-sheet-one-path": "a country sheet, and an Indian state or union territory sheet, each draw through ONE path (@map/packet/countrySheet.js; country-posters/src/indiaStateSheet.js), so one place cannot print two posters. Parses (comments cannot count) plugin sources: a direct generateCountrySheet FAILS anywhere, a direct generateSheet FAILS inside country-posters outside the state helper, every makeCountrySheet call must pass `countries`, and the known callers must be found. Named data answers: seriesFor over country-posters.json (India 1 of 1, each 3x3 country N of 173, an absent code no series, exactly one no-sheet country); over the served india-states.json + gb-IND-ADM1.geojson: 36 divisions, the 34 with groups numbered 1..34 each with an outline, the rest named, the shared ADM1 never mutated.",
|
||||
"check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.",
|
||||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||
|
|
@ -577,11 +580,12 @@
|
|||
"check:print-frame-reclaim": "a print frame is kept until it has printed: openPrintFrame reclaims it 60 s after its load (not after its creation), at afterprint, or at a 10-minute ceiling, and printImageBlob revokes its object URL on the same clock. Fake timers; two-sided (the pre-fix copy fails). The Wall poster loads 132 MB of photos, and the old 60 s-from-creation clock removed the frame before it could print.",
|
||||
"check:poster-card-fits": "a poster card's print-options row must stay inside its own card. Reported by the repo agent off a 1440px screenshot: 27/27 cards overflowing by 110px, 36 neighbour intrusions. .pfs already had width:82%, so SOURCE READING SAID IT WAS FINE — flex items default to min-width:auto and refuse to shrink. Asserts BOTH no-overlap AND that the controls keep a usable box, because a row whose buttons collapsed to nothing would also \"not overflow\". Sweeps 390/768/1024/1440/1920px and asserts the row sits BELOW the preview too — measuring only left/right edges is how it passed a driver-visible regression. CARD_FIT_WIDTH overrides the list. SKIPs when nothing is served.",
|
||||
"check:poster-degrades": "with NO stored poster, NO source and NO generator (forced in the browser: every posters/*.webp and *.html answers 404, lib/poster-render.js is aborted), nothing must print and the reader must be told why. Measures any served tree; NOT MEASURED without a wall url.",
|
||||
"check:poster-design-catalogue": "Poster Design's configuration catalogue (plugin-designs/poster-design/src/templates.js) says only true things about the library: its sheet formats are exactly poster-render.js FORMATS at the same pixel sizes (the generator is a pinned asset, so the table cannot be imported); every configuration name is unique and well formed; the card entries are exactly card-render STOCK; every catalogue key it names exists in en/posterDesign.json; the sample country can be drawn. Six mutants (px drift, a missing format, a duplicate name, a bad key, an undrawable sample, a dropped stock) each FAIL.",
|
||||
"check:poster-fetch-guard": "a host's 200 fallback page is never taken for a poster. On a drop-in dev host a missing posters/<id>.webp answered 200 text/html, and export handed that page to the print dialog as the image. Asserts library/packet/poster-fetch.js accepts a raster only if it is an image (by type or by its first bytes) and a source only if it is a poster document (class=\"stage\"), and that neither usePosterPrints nor PosterWall fetches posters/ directly. Node only, no server.",
|
||||
"check:poster-layout-optin": "the composed poster sheet (header, map, legend beside it, footer) is OPT-IN: spec.layout === 'poster' asks for it, and every caller that does not gets the full-page map exactly as before, asserted by object identity, not just by value. Drives the real layout decisions with real spec objects. Headless, no browser.",
|
||||
"check:poster-print-fallback": "clicks the REAL poster print control with the served poster generator and every posters/*.webp aborted, and asserts the preview RASTERISES from posters/<id>.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; NOT MEASURED otherwise.",
|
||||
"check:posters-exportable": "every poster the tree offers can be printed. With posterSources on, the tree once shipped only the 28 zone sources, so 350 of 378 posters said \"poster not found (404)\". Each id, from data/posters and the wall's previews, must ship a real WebP (RIFF..WEBP) or a real poster source (its class=\"stage\" element), so a placeholder or a host's fallback page cannot pass. SKIPs when posters are off by config, which check:poster-degrades covers. Reads the built tree; no server.",
|
||||
"check:posterverse-shelf": "Posterverse (the capability-5 poster shell) shows exactly its allow-list and never a gated plugin. Asserts the rail rows per section (Posters: upg-100, country-posters, poster-design; Experimental: wall-of-people-groups, all-people-groups, upg-neighbor, bible-posters), that no row names key-cities or a shell (a key-cities bundle is planted into the built manifest, because the real one withholds gated profiles), that every present row opens the profile it names (no silent fallback to a leaf), that posterverse.meta.json composes equals the rows whose bundle exists, and 11-locale parity of the shell strings. --self-test plants one defect per arm. Headless. Exit 0 pass, 1 fail, 75 no built manifest.",
|
||||
"check:posterverse-shelf": "Posterverse (the capability-5 poster shell) shows exactly its allow-list and never a gated plugin. Asserts the rail rows per section (Posters: upg-100, country-posters, poster-design; Experimental: wall-of-people-groups, bible-posters), that no row names key-cities or a shell (a key-cities bundle is planted into the built manifest, because the real one withholds gated profiles), that every present row opens the profile it names (no silent fallback to a leaf), that posterverse.meta.json composes equals the rows whose bundle exists, and 11-locale parity of the shell strings. --self-test plants one defect per arm. Headless. Exit 0 pass, 1 fail, 75 no built manifest.",
|
||||
"check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT=<dir> so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.",
|
||||
"check:preset-config-merge": "a preset page (<bundle>/<profile>/<instance>/index.html) keeps every default in its profile's .meta.json config, with the instance's props merged over them (go-int's upg-neighbor presets lost pinHint:false when props replaced the config). Reads the built tree; SKIP without one.",
|
||||
"check:profile-table": "the prayer packet's profile TABLE (20 full-width rows: picture, generated description, three boxes; PER_PAGE stays 20): a row stays tall enough for a name plus a two-line description at the smallest fit size, and truncation happens only at a whole word. Character-count, not DOM, so Node and the browser agree. Cannot see the RENDERED row height: that is check:packet-render, a browser gate. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",
|
||||
|
|
@ -664,7 +668,9 @@
|
|||
"pin:poster-render": "rewrite library/packet/poster-render.integrity.js from library/packet/poster-render.js — run after any change to the generator; check:poster-render-pinned fails until it is.",
|
||||
"check:board-map-options": "the GEOGRAPHIC map caller must state whiteboard/boundaries EXPLICITLY, never inherit them. library/maps/whiteboard-map.js defaults to whiteboard:true/boundaries:false (it was extracted for the whiteboard case), the inverse of what a real map needs, so a caller relying on defaults renders a blank basemap. check:gospel-whiteboard cannot see this — it asserts chart geometry and imports no component.",
|
||||
"check:popup-pan-controls": "the popup auto-pan reserves the map's bottom control band, so a popup that clears the CLIP is also clear of the things that take the click. Measured at 390 on a 366x732 map: the pan fitted the popup inside the container, cut 0, and a real click on Full Profile still did nothing because the top element at the button's centre was div.maplibregl-ctrl-attrib-inner (and span.pin-hint-pill on the sibling pin) - a cut-height check cannot see this, because the cut really was zero. Attribution is a licence term of the basemap, so the popup may not be raised over it; the usable frame is shorter than the container and the fit test says so. Runs the SHIPPED panPlanFor, read out of the SFC with new Function (it is not exported), never a re-typed copy, and asserts where the popup ENDS UP rather than re-deriving the expected number from the same formula. Sweeps band heights 0-120px and every width the component renders at. Source-only, headless.",
|
||||
"check:control-inset": "bottomControlInset MEASURES the map's real bottom control band, so the pan reserves the band the controls actually occupy. check:popup-pan-controls proves the ARITHMETIC; this proves the band is real. If the inset silently returned 0 -- a selector typo, an engine renaming its control classes, a control moving outside the container -- panPlanFor would get bottomInset 0, behave exactly as it did before the fix, and check:popup-pan-controls would still pass every case, because its arithmetic is still correct: the bug back with both gates green. 8 cases against a real DOM, running the SHIPPED bottomControlInset read out of the SFC (it is not exported), never a copy: the band equals what the DOM independently says (not merely non-zero); no controls reserves 0; a zero-sized control reserves nothing; the taller of two corners wins; a wrapped attribution reserves more than one line; a control outside the container is ignored; a null container returns 0; and the MAPBOX corners count too, since the component takes a provider prop and every other case renders MapLibre names. Needs a browser: every assertion rests on getBoundingClientRect. 75 = no slot, never a pass."
|
||||
"check:control-inset": "bottomControlInset MEASURES the map's real bottom control band, so the pan reserves the band the controls actually occupy. check:popup-pan-controls proves the ARITHMETIC; this proves the band is real. If the inset silently returned 0 -- a selector typo, an engine renaming its control classes, a control moving outside the container -- panPlanFor would get bottomInset 0, behave exactly as it did before the fix, and check:popup-pan-controls would still pass every case, because its arithmetic is still correct: the bug back with both gates green. 8 cases against a real DOM, running the SHIPPED bottomControlInset read out of the SFC (it is not exported), never a copy: the band equals what the DOM independently says (not merely non-zero); no controls reserves 0; a zero-sized control reserves nothing; the taller of two corners wins; a wrapped attribution reserves more than one line; a control outside the container is ignored; a null container returns 0; and the MAPBOX corners count too, since the component takes a provider prop and every other case renders MapLibre names. Needs a browser: every assertion rests on getBoundingClientRect. 75 = no slot, never a pass.",
|
||||
"check:panel-sheets": "the shared panel geometry (library/components/poster/panelSheets.js) for posters printed on several sheets: 60x96 = 3 × 60×36 stacked and 66x66 = 2 × 36×66 side by side, 6 in joins, and a single sheet; the sheets tile the finished poster, glue slices tile and bleed slices repeat exactly the join, a bleed join prints a guide not a solid band, a screen preview always shows the red band, and the Bless 5x8ft takes its bands and marks from the module. Two-sided (4 planted defects). Headless.",
|
||||
"check:poster-wall-host-seams": "PosterWall's two host seams, and that with neither set every wall behaves as before. headerAction: default null is the Show-on-map TOGGLE (aria-pressed, one window event per click, is-last, no emit); { labelKey, ariaKey } is a plain button that emits header-action with the item, no window event, no aria-pressed, and reads the label alone for an item with no count. previewSrc: default null is the stored previews/<id>.webp and no IntersectionObserver; set, a card renders only once in view of the wall's own scroller, one render at a time, cached per page, and null / a throw / a timeout ends on the preview-unavailable placeholder. Image errors: a default wall retries the tree's own copy before the placeholder; a generated preview goes straight to it. Headless: PosterWall compiled as a client component on a recording renderer; real onClick and a recording IntersectionObserver. 15 mutants each FAIL."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.6.9",
|
||||
|
|
|
|||
BIN
plugin-designs/country-posters/assets/previews/india-an.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
plugin-designs/country-posters/assets/previews/india-ap.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
plugin-designs/country-posters/assets/previews/india-ar.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
plugin-designs/country-posters/assets/previews/india-as.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
plugin-designs/country-posters/assets/previews/india-br.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
plugin-designs/country-posters/assets/previews/india-ch.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
plugin-designs/country-posters/assets/previews/india-ct.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
plugin-designs/country-posters/assets/previews/india-dh.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
plugin-designs/country-posters/assets/previews/india-dl.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
plugin-designs/country-posters/assets/previews/india-ga.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |