Merge origin/development (53fcf6d09) into studio/p-assembly

This commit is contained in:
Doxa Maps 2026-10-01 05:32:16 +00:00
commit 77751ddcb6
207 changed files with 1250 additions and 966 deletions

138
README.md
View file

@ -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.**
![Desktop app (Tauri) — proven, not yet in production](documentation/readme-images/deploy5-desktop.png)
![Desktop app (Tauri), working](documentation/readme-images/deploy5-desktop.png)
**Rudder, the voice-to-text plugin, runs as its own desktop app too**, built with
`bun run desktop:build rudder-voice` ([HOWTO-build-the-desktop-app](./documentation/HOWTO-build-the-desktop-app.md)).
![Rudder as a desktop app on Linux: offline voice-to-text](documentation/readme-images/deploy5-desktop-rudder.png)
![Rudder, the offline voice-to-text desktop app, running](documentation/readme-images/deploy5-desktop-rudder.png)
<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.
![The delegation model: main at the centre guarded by a merge queue, inside the Plugin Studio; eight plugins around it; one agent per plugin; and one shepherd over all the agents](documentation/readme-images/delegation-model.png)
**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.
![The delegation model: development at the centre behind a landing lock, inside the Plugin Studio; eight plugins around it, each owned by one agent; a coordinator and a gate-keeper over the agents; below, how a change lands in four steps, and the three outputs](documentation/readme-images/delegation-model.png)
<sub>[Open the diagram full size](documentation/readme-images/delegation-model.png): on a phone its small text needs it.</sub>
### 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
data/posters/india-ap.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
data/posters/india-ar.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
data/posters/india-as.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
data/posters/india-br.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
data/posters/india-ch.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
data/posters/india-ct.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
data/posters/india-dh.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
data/posters/india-dl.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
data/posters/india-ga.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
data/posters/india-gj.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
data/posters/india-hp.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
data/posters/india-hr.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
data/posters/india-jh.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
data/posters/india-jk.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
data/posters/india-ka.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

BIN
data/posters/india-kl.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

BIN
data/posters/india-la.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
data/posters/india-ld.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
data/posters/india-mh.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
data/posters/india-ml.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
data/posters/india-mn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
data/posters/india-mp.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
data/posters/india-or.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
data/posters/india-pb.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
data/posters/india-py.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
data/posters/india-rj.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
data/posters/india-sk.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
data/posters/india-tg.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
data/posters/india-tn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

BIN
data/posters/india-tr.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
data/posters/india-up.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
data/posters/india-ut.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
data/posters/india-wb.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 KiB

After

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -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

View file

@ -86,6 +86,32 @@ ok(dry(formatClipboard(arabic, { color: '#0072b2' })) !== 'ok', 'NEGATIVE CONTRO
const indiaText = X.selectionClipboard([{ key: 'adm1:Mahārāshtra', name: 'Mahārāshtra', count: 54, kind: 'adm1' }, { key: 'adm1:Goa', name: 'Goa', count: 5, kind: 'adm1' }], {})
ok(dry(indiaText) === 'ok', `apply-grouping's dry run refuses an India state selection: ${dry(indiaText)}`, 'an India state selection resolves too (shape names are canonical)')
// ── A3. the client's regroupable() agrees with apply-grouping's own verdict ─────────────────────────────
// Copy warns when its first line will not regroup (a mix of kinds, or a legend zone). The warning is only honest if
// regroupable() says exactly what the tool's dry run says, so each case is run through BOTH.
const T = (key, name, count, kind, country) => ({ key, name, count, kind, ...(country ? { country } : {}) })
const REGROUP_CASES = {
'India states': [T('adm1:Mahārāshtra', 'Mahārāshtra', 54, 'adm1', 'IND'), T('adm1:Goa', 'Goa', 5, 'adm1', 'IND')],
'a China province': [T('adm1:Gansu Province', 'Gansu Province', 3, 'adm1', 'CHN')],
'countries': [T('adm0:NPL', 'Nepal', 34, 'adm0'), T('adm0:BTN', 'Bhutan', 13, 'adm0')],
'a state + a country': [T('adm1:Goa', 'Goa', 5, 'adm1', 'IND'), T('adm0:NPL', 'Nepal', 34, 'adm0')],
'India + China': [T('adm1:Goa', 'Goa', 5, 'adm1', 'IND'), T('adm1:Gansu Province', 'Gansu Province', 3, 'adm1', 'CHN')],
'a legend zone': [T('zone:test', 'Test Zone', 12, 'zone')],
}
const disagree = []
for (const [name, sel] of Object.entries(REGROUP_CASES)) {
const tool = dry(X.selectionClipboard(sel, {})) === 'ok'
if (X.regroupable(sel) !== tool) disagree.push(`${name}: regroupable ${X.regroupable(sel)}, apply-grouping ${tool ? 'accepts' : 'refuses'}`)
}
// …and the COUNTRY an ADM1 territory needs for that verdict is always known: from the feature's own props on a
// map tap, else from the baked table (a props-only toggle once left it null, which read as a false warning).
const { resolveTerritory, ADM1_COUNTS, ADM1_COUNTRY } = await import(pathToFileURL(P('plugin-designs/my-upg-100-list/data/territory-counts.js')).href)
const noCountry = Object.keys(ADM1_COUNTS).filter((n) => !['IND', 'CHN'].includes(ADM1_COUNTRY[n]))
ok(!noCountry.length && resolveTerritory({ shapeName: 'Mahārāshtra' }).country === 'IND' && resolveTerritory({ shapeName: 'Guizhou Province' }).country === 'CHN',
`an ADM1 territory without a known country: ${noCountry.slice(0, 5).join(', ') || 'a props-only toggle'}`, 'every ADM1 name resolves its country, even from a props-only toggle')
ok(!disagree.length, `Copy's regroup warning disagrees with apply-grouping: ${disagree.join('; ')}`,
`the regroup warning matches apply-grouping's own verdict in all ${Object.keys(REGROUP_CASES).length} cases (India, China, countries, and the three it refuses)`)
// ── B. the CSV ───────────────────────────────────────────────────────────────────────────────────
function parseCsv(s) { // RFC 4180: quoted fields, doubled quotes, CRLF records
const rows = []; let row = [], cell = '', q = false
@ -124,6 +150,17 @@ const wired = {
'the prefetch on batch mode': /if \(next\) selectionRecordsLoad\(\)/.test(vue),
'one detection for every action': (vue.match(/await selectionPeople\(\)/g) || []).length >= 2 && /selectionPeople\(\)\.then/.test(vue),
// India's states resolve through the served table (library/data/indiaStates.js), fetched only once a state is selected
// In batch mode a tap on a pin opens NO detail sheet (it covered the whole HUD on a phone); the tapestry
// listener under it toggles the territory. The real-tap proof is rega's; this pins the guard in the source.
'a pin tap in batch mode opens no sheet': /map\.on\('click', 'upg-pins-hit', \(e\) => \{[\s\S]{0,700}?if \(batchMode\.value\) return\s*\n\s*const f = e && e\.features/.test(vue),
// …and that tap toggles the territory EXACTLY ONCE: the tapestry listener already does it, so the pin handler
// must not toggle a second time (select-then-deselect in one tap).
'the pin handler adds no second toggle': (() => {
const at = vue.indexOf("map.on('click', 'upg-pins-hit', (e) => {"); if (at < 0) return false
const body = vue.slice(at, vue.indexOf('\n })', at))
return !/toggleTerritory|toggleByProps|toggleZoneSelection/.test(body)
})(),
'the regroup warning on Copy': /const warn = regroupable\(sel\) \? '' : t\('upgMap\.copyNotRegroupable'\)/.test(vue),
'India states detected from the served table': /import \{ loadIndiaStates \} from '@map\/data\/indiaStates\.js'/.test(vue)
&& /india\.byShapeName\.get\(t\.name\)/.test(vue) && /t\.kind === 'adm1'\) indiaStates\(\)\.catch/.test(vue),
}
@ -133,7 +170,7 @@ ok(!missingWires.length, `the HUD is missing: ${missingWires.join(', ')}`, 'the
// ── D. the strings, ×11 ──────────────────────────────────────────────────────────────────────────
const LANGS = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh']
const KEYS = {
upgMap: ['copySelection', 'exportCsv', 'selectionColourGroup', 'selectionColour', 'customColour', 'copiedSelection', 'copiedTerritoriesOnly', 'csvSaved', 'csvFailed'] }
upgMap: ['copySelection', 'exportCsv', 'copyNotRegroupable', 'selectionColourGroup', 'selectionColour', 'customColour', 'copiedSelection', 'copiedTerritoriesOnly', 'csvSaved', 'csvFailed'] }
const cat = (l, f) => JSON.parse(readFileSync(P('library/i18n/locales', l, `${f}.json`), 'utf8'))
const bad = []
for (const [f, keys] of Object.entries(KEYS)) {

View file

@ -38,6 +38,10 @@
* 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.
* J (section heads) a section with people (total > 0) reads "<n people groups> · <items>", as it always has;
* a section with none (a template gallery) reads "<items>" alone: no 0 count, no " · ".
* K (sectionItemCountKey) a section the callback names is counted in its own words and number ({ key, n });
* a section it answers null for keeps the default count.
* 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.
@ -337,6 +341,39 @@ try {
}
w.app.unmount()
}
// J: a section head names its people-group total only when there is one
{
const groups = { j1: 'People', j2: 'People', j3: 'Templates', j4: 'Templates' }
const root = node('root')
const app = createApp(PosterWall, { instanceId: 'gate-j', groupBy: (x) => groups[x.id],
items: [{ id: 'j1', label: 'Jone', count: 5 }, { id: 'j2', label: 'Jtwo', count: 2 }, { id: 'j3', label: 'Jthree' }, { id: 'j4', label: 'Jfour' }] })
app.mount(root); await rc.nextTick()
const heads = []
walk(root, (n) => { if (hasClass(n, 'sec-count')) heads.push(textOf(n).trim()) })
if (heads.length !== 2) fail(`J: expected 2 section heads, found ${heads.length}`)
const withPeople = heads.find((h) => /\b7\b/.test(h))
const without = heads.find((h) => !/\b7\b/.test(h))
if (!withPeople || !/·/.test(withPeople) || !/people/i.test(withPeople)) fail(`J: a section with people should read "<n> people groups · <items>", got ${JSON.stringify(withPeople)}`)
if (!without || /·/.test(without) || /\b0\b/.test(without) || /people/i.test(without)) fail(`J: a section with no people should read its items alone, got ${JSON.stringify(without)}`)
app.unmount()
}
// K: one section counted in its own words (India: its states, not countries); the others unchanged
{
const groups = { k1: 'India', k2: 'India', k3: 'India', k4: 'Asia', k5: 'Asia' }
const root = node('root')
const app = createApp(PosterWall, { instanceId: 'gate-k', groupBy: (x) => groups[x.id],
sectionItemCountKey: (sec) => (sec.name === 'India' ? { key: 'regions.countryCount', n: 5 } : null),
items: Object.keys(groups).map((id, i) => ({ id, label: id, count: i + 1 })) })
app.mount(root); await rc.nextTick()
const heads = {}
walk(root, (n) => { if (hasClass(n, 'sec-head')) heads[textOf(n).includes('India') ? 'India' : 'Asia'] = textOf(n) })
if (!/\b5 countries\b/.test(heads.India || '')) fail(`K: the named section should count 5 in its own key, got ${JSON.stringify(heads.India)}`)
if (/\b3 list/.test(heads.India || '')) fail('K: the named section still shows the default count')
if (!/\b2 lists?\b/.test(heads.Asia || '')) fail(`K: an unnamed section should keep the default count, got ${JSON.stringify(heads.Asia)}`)
app.unmount()
}
} catch (e) {
fail(`threw: ${e?.stack || e}`)
} finally {
@ -348,5 +385,5 @@ 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)`)
console.log(`POSTER-WALL-HOST-SEAMS: PASS (11 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)`)
}

View file

@ -77,6 +77,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 },
@ -197,6 +201,11 @@ const props = defineProps({
// 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 },
// ★ ONE SECTION COUNTED IN ITS OWN WORDS. null (the default) counts every section's cards with item-count-key, as
// always. A wall whose section holds a different kind of card passes (sec) => an i18n plural key, or { key, n }
// to count n rather than the section's card count, or null for that section to keep the default. The Country
// Posters India section holds India's own card and one per state and union territory, which are not countries.
sectionItemCountKey: { type: Function, default: null },
})
const emit = defineEmits(['header-action'])
@ -307,6 +316,17 @@ const hud = (key, en, params = null, n = undefined) => {
return v && v !== key ? v : String(en).replace(/\{(\w+)\}/g, (m, k) => (params && params[k] != null ? String(params[k]) : m))
}
const itemCount = (n) => (props.itemCountKey ? countOf(props.itemCountKey, n) : `${n} ${n === 1 ? props.itemNoun : plural.value}`)
// A section head's count line. The people-group total and its " · " leave together when the total is 0 (a gallery
// of poster templates counts no people), the rule the cards already follow with v-if="item.count". One string in
// one interpolation, so a head with a total renders exactly the text it always did, whitespace included.
const sectionItems = (sec) => {
const own = props.sectionItemCountKey ? props.sectionItemCountKey(sec) : null
if (!own) return itemCount(sec.items.length)
return typeof own === 'string' ? countOf(own, sec.items.length) : countOf(own.key, own.n ?? sec.items.length)
}
const sectionCount = (sec) => (sec.total > 0
? `${countOf('wall.peopleGroupCount', sec.total)} · ${sectionItems(sec)}`
: sectionItems(sec))
const base = computed(() => String(props.assetsBase || './assets').replace(/\/+$/, ''))
// ★ A SECOND PLACE TO LOOK, because "poster not generated yet" was being shown for posters that
@ -348,7 +368,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)
})
@ -1310,7 +1330,7 @@ useShadowStyles(
<span v-if="collapsible" class="sec-caret" :class="{ 'is-open': !isCollapsed(sec.name) }">▶</span>
<h3 class="sec-name">{{ sec.name }}</h3>
<span class="sec-count">
{{ countOf('wall.peopleGroupCount', sec.total) }} · {{ itemCount(sec.items.length) }}
{{ sectionCount(sec) }}
</span>
</component>

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "الهند — الولايات والأقاليم الاتحادية",
"indiaStateNoGroups": "لا توجد هنا شعوب غير مُبشَّرة مُدرَجة."
"indiaNote": "للهند قسمها الخاص في المقدّمة: عدد الشعوب غير المُبشَّرة فيها {count}، وهو أكثر بكثير من أي بلد آخر، لذا لكل ولاية وإقليم اتحادي فيها ملصقه الخاص.",
"indiaStateNoGroups": "لا توجد هنا شعوب غير مُبشَّرة مُدرَجة.",
"indiaItemCount": "لا ولايات ولا أقاليم | ولاية أو إقليم اتحادي واحد | ولايتان أو إقليمان | {n} ولايات وأقاليم اتحادية | {n} ولاية وإقليمًا اتحاديًا | {n} ولاية وإقليم اتحادي"
}

View file

@ -46,5 +46,6 @@
"renderer": "المولّد",
"legacy": "مقاس قديم: ما زال يُطبع، لكن أداة اختيار الطباعة لم تعد تعرضه.",
"forAgents": "لاستخدام هذا القالب، قل للوكيل: استخدم الإعداد {name}."
}
},
"templateCount": "لا قوالب | قالب واحد | قالبان | {n} قوالب | {n} قالبًا | {n} قالب"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "تم حفظ CSV — الشعوب: {count}",
"csvFailed": "فشل تصدير CSV: {reason}",
"copySelection": "نسخ",
"exportCsv": "تصدير CSV"
"exportCsv": "تصدير CSV",
"copyNotRegroupable": "لا يمكن إعادة تجميعها معًا: انسخ ولايات أو مقاطعات بلد واحد، أو البلدان، كلًّا على حدة",
"displayName": "ملصقات قائمة UPG 100",
"wallHeading": "ملصقات قائمة UPG 100"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "Indien – Bundesstaaten und Unionsterritorien",
"indiaStateNoGroups": "Hier sind keine unerreichten Volksgruppen verzeichnet."
"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.",
"indiaItemCount": "{n} Bundesstaat oder Unionsterritorium | {n} Bundesstaaten und Unionsterritorien"
}

View file

@ -46,5 +46,6 @@
"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}."
}
},
"templateCount": "{n} Vorlage | {n} Vorlagen"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV gespeichert — Volksgruppen: {count}",
"csvFailed": "CSV-Export fehlgeschlagen: {reason}",
"copySelection": "Kopieren",
"exportCsv": "CSV exportieren"
"exportCsv": "CSV exportieren",
"copyNotRegroupable": "Nicht gemeinsam umgruppierbar: Bundesstaaten oder Provinzen eines Landes oder die Länder getrennt kopieren",
"displayName": "Poster der UPG 100 Liste",
"wallHeading": "Poster der UPG 100 Liste"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "India — states and union territories",
"indiaStateNoGroups": "No unreached people groups are listed here."
"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.",
"indiaItemCount": "{n} states and union territories | {n} state or union territory | {n} states and union territories"
}

View file

@ -46,5 +46,6 @@
"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}."
}
},
"templateCount": "{n} templates | {n} template | {n} templates"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV saved — people groups: {count}",
"csvFailed": "CSV failed: {reason}",
"copySelection": "Copy",
"exportCsv": "Export CSV"
"exportCsv": "Export CSV",
"copyNotRegroupable": "Can't be regrouped together: copy one country's states or provinces, or the countries, separately",
"displayName": "UPG 100 List Posters",
"wallHeading": "UPG 100 List Posters"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "India: estados y territorios de la unión",
"indiaStateNoGroups": "Aquí no figura ningún pueblo no alcanzado."
"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.",
"indiaItemCount": "{n} estado o territorio de la unión | {n} de estados y territorios de la unión | {n} estados y territorios de la unión"
}

View file

@ -46,5 +46,6 @@
"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}."
}
},
"templateCount": "{n} plantilla | {n} de plantillas | {n} plantillas"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV guardado — pueblos: {count}",
"csvFailed": "Error al exportar el CSV: {reason}",
"copySelection": "Copiar",
"exportCsv": "Exportar CSV"
"exportCsv": "Exportar CSV",
"copyNotRegroupable": "No se pueden reagrupar juntos: copia por separado los estados o provincias de un solo país, o los países",
"displayName": "Carteles de la lista UPG 100",
"wallHeading": "Carteles de la lista UPG 100"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "Inde — États et territoires de l’Union",
"indiaStateNoGroups": "Aucun peuple non atteint n’est répertorié ici."
"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.",
"indiaItemCount": "{n} État ou territoire de l’Union | {n} d’États et territoires de l’Union | {n} États et territoires de l’Union"
}

View file

@ -46,5 +46,6 @@
"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}."
}
},
"templateCount": "{n} modèle | {n} de modèles | {n} modèles"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV enregistré — peuples : {count}",
"csvFailed": "Échec de l’export CSV : {reason}",
"copySelection": "Copier",
"exportCsv": "Exporter en CSV"
"exportCsv": "Exporter en CSV",
"copyNotRegroupable": "Impossible de les regrouper ensemble : copiez séparément les États ou provinces d’un seul pays, ou les pays",
"displayName": "Affiches de la liste UPG 100",
"wallHeading": "Affiches de la liste UPG 100"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "भारत — राज्य और केंद्र शासित प्रदेश",
"indiaStateNoGroups": "यहाँ कोई अपहुँचा जनसमूह सूचीबद्ध नहीं है।"
"indiaNote": "भारत का अपना अलग खंड है, सबसे पहले: इसमें {count} अपहुँचे जनसमूह हैं, किसी भी अन्य देश से कहीं अधिक, इसलिए इसके हर राज्य और केंद्र शासित प्रदेश का अपना पोस्टर है।",
"indiaStateNoGroups": "यहाँ कोई अपहुँचा जनसमूह सूचीबद्ध नहीं है।",
"indiaItemCount": "{n} राज्य या केंद्र शासित प्रदेश | {n} राज्य और केंद्र शासित प्रदेश"
}

View file

@ -46,5 +46,6 @@
"renderer": "जनरेटर",
"legacy": "पुराना आकार: यह अब भी प्रिंट होता है, पर प्रिंट चयनकर्ता अब इसे नहीं दिखाता।",
"forAgents": "यह टेम्पलेट इस्तेमाल करने के लिए किसी एजेंट से कहें: कॉन्फ़िगरेशन {name} इस्तेमाल करो।"
}
},
"templateCount": "{n} टेम्पलेट | {n} टेम्पलेट"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV सहेजा गया — जनसमूह: {count}",
"csvFailed": "CSV निर्यात विफल: {reason}",
"copySelection": "कॉपी करें",
"exportCsv": "CSV निर्यात करें"
"exportCsv": "CSV निर्यात करें",
"copyNotRegroupable": "इन्हें एक साथ पुनर्समूहित नहीं किया जा सकता: एक देश के राज्य या प्रांत, या देश, अलग-अलग कॉपी करें",
"displayName": "UPG 100 सूची पोस्टर",
"wallHeading": "UPG 100 सूची पोस्टर"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "India — stati e territori dell’Unione",
"indiaStateNoGroups": "Qui non è elencato alcun popolo non raggiunto."
"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.",
"indiaItemCount": "{n} stato o territorio dell’Unione | {n} di stati e territori dell’Unione | {n} stati e territori dell’Unione"
}

View file

@ -46,5 +46,6 @@
"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}."
}
},
"templateCount": "{n} modello | {n} di modelli | {n} modelli"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV salvato — popoli: {count}",
"csvFailed": "Esportazione CSV non riuscita: {reason}",
"copySelection": "Copia",
"exportCsv": "Esporta CSV"
"exportCsv": "Esporta CSV",
"copyNotRegroupable": "Non si possono raggruppare insieme: copia separatamente gli stati o le province di un solo paese, o i paesi",
"displayName": "Poster della lista UPG 100",
"wallHeading": "Poster della lista UPG 100"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "Índia — estados e territórios da união",
"indiaStateNoGroups": "Nenhum povo não alcançado está listado aqui."
"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.",
"indiaItemCount": "{n} estado ou território da união | {n} de estados e territórios da união | {n} estados e territórios da união"
}

View file

@ -46,5 +46,6 @@
"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}."
}
},
"templateCount": "{n} modelo | {n} de modelos | {n} modelos"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV salvo — povos: {count}",
"csvFailed": "Falha ao exportar o CSV: {reason}",
"copySelection": "Copiar",
"exportCsv": "Exportar CSV"
"exportCsv": "Exportar CSV",
"copyNotRegroupable": "Não podem ser reagrupados juntos: copie separadamente os estados ou províncias de um só país, ou os países",
"displayName": "Cartazes da lista UPG 100",
"wallHeading": "Cartazes da lista UPG 100"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "India — state și teritorii ale Uniunii",
"indiaStateNoGroups": "Aici nu este listat niciun popor neatins."
"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.",
"indiaItemCount": "un stat sau teritoriu al Uniunii | {n} state și teritorii ale Uniunii | {n} de state și teritorii ale Uniunii"
}

View file

@ -46,5 +46,6 @@
"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}."
}
},
"templateCount": "{n} șablon | {n} șabloane | {n} de șabloane"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV salvat — popoare: {count}",
"csvFailed": "Exportul CSV a eșuat: {reason}",
"copySelection": "Copiază",
"exportCsv": "Exportă CSV"
"exportCsv": "Exportă CSV",
"copyNotRegroupable": "Nu pot fi regrupate împreună: copiază separat statele sau provinciile unei singure țări, sau țările",
"displayName": "Afișe din lista UPG 100",
"wallHeading": "Afișe din lista UPG 100"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "Индия — штаты и союзные территории",
"indiaStateNoGroups": "Здесь не указано ни одного недостигнутого народа."
"indiaNote": "У Индии свой раздел, первый: число недостигнутых народов в ней — {count}, намного больше, чем в любой другой стране, поэтому у каждого её штата и союзной территории есть свой плакат.",
"indiaStateNoGroups": "Здесь не указано ни одного недостигнутого народа.",
"indiaItemCount": "{n} штат или союзная территория | {n} штата и союзные территории | {n} штатов и союзных территорий | {n} штата и союзной территории"
}

View file

@ -46,5 +46,6 @@
"renderer": "Генератор",
"legacy": "Устаревший размер: он ещё печатается, но в выборе печати его больше нет.",
"forAgents": "Чтобы использовать этот шаблон, скажите агенту: используй конфигурацию {name}."
}
},
"templateCount": "{n} шаблон | {n} шаблона | {n} шаблонов | {n} шаблонов"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV сохранён — народы: {count}",
"csvFailed": "Не удалось экспортировать CSV: {reason}",
"copySelection": "Копировать",
"exportCsv": "Экспорт в CSV"
"exportCsv": "Экспорт в CSV",
"copyNotRegroupable": "Нельзя перегруппировать вместе: копируйте отдельно штаты или провинции одной страны либо страны",
"displayName": "Плакаты списка UPG 100",
"wallHeading": "Плакаты списка UPG 100"
}

View file

@ -1,4 +1,5 @@
{
"indiaStatesHeading": "印度——邦与中央直辖区",
"indiaStateNoGroups": "此处未列出任何未得之民族群。"
"indiaNote": "印度有单独的一栏,排在最前:这里有 {count} 个未得之民族群,远多于任何其他国家,因此它的每个邦和中央直辖区都有自己的海报。",
"indiaStateNoGroups": "此处未列出任何未得之民族群。",
"indiaItemCount": "{n} 个邦和中央直辖区"
}

View file

@ -46,5 +46,6 @@
"renderer": "生成器",
"legacy": "旧尺寸:仍可打印,但打印选择器已不再提供。",
"forAgents": "要使用此模板,请告诉代理:使用配置 {name}。"
}
},
"templateCount": "{n} 个模板"
}

View file

@ -14,5 +14,8 @@
"csvSaved": "CSV 已保存 — 族群:{count}",
"csvFailed": "CSV 导出失败:{reason}",
"copySelection": "复制",
"exportCsv": "导出 CSV"
"exportCsv": "导出 CSV",
"copyNotRegroupable": "无法一起重新分组:请分别复制同一国家的邦或省,或各个国家",
"displayName": "UPG 100 名单海报",
"wallHeading": "UPG 100 名单海报"
}

View file

@ -662,7 +662,7 @@
"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: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."
"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. Section heads: a section with people reads \"<n> people groups · <items>\" exactly as before; one with none reads its items alone. sectionItemCountKey lets a wall count one section in its own words ({ key, n }); unnamed sections keep the default. Headless: PosterWall compiled as a client component on a recording renderer; real onClick and a recording IntersectionObserver. 17 mutants each FAIL."
},
"devDependencies": {
"@capacitor/android": "^7.6.9",

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

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