Merge remote-tracking branch 'origin/development' into regions/country-sheet-onedef
# Conflicts: # public/dx-maps/doxa-maps-spa/manifest.json # public/dx-maps/manifest.json # public/dx-maps/regions-dashboard/region-dashboards/index.html # public/dx-maps/regions-dashboard/regions-dashboard.js # public/dx-maps/regions-dashboard/regions-list/index.html # public/dx-maps/regions-dashboard/regions-mfe/index.html
64
README.md
|
|
@ -62,7 +62,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 `main`** | [agent swarm](#build-it-with-an-agent-swarm) |
|
||||
| **One agent per plugin, one shepherd, one working `development`** | [agent swarm](#build-it-with-an-agent-swarm) |
|
||||
|
||||
## For vibe coders: build, open the Studio, embed a map
|
||||
|
||||
|
|
@ -76,17 +76,21 @@ bun run dev # open the address it prints: that page is the P
|
|||
**2. Build.** The built maps are already in the repo. After a change, rebuild just that map, or all of them:
|
||||
|
||||
```bash
|
||||
BUNDLE=<map> bun x vite build # one map (seconds)
|
||||
BUILD_ALLOW=1 bun run build # every map (~4 min); the output is public/dx-maps/
|
||||
BUNDLE=<map> bun x vite build && bun run tree && bun run fix:sri # one map, then its pages and hashes
|
||||
BUILD_ALLOW=1 bun run build # every map (~4 min); the output is public/dx-maps/
|
||||
```
|
||||
|
||||
When the DX Life site's repo sits next to this one (`../doxa-life`), the full build also copies the maps into
|
||||
After a one-map build, `bun run tree` rewrites the map's pages and the manifest (a changed `.meta.json` reaches
|
||||
them only this way), and `fix:sri` refreshes their integrity hashes. Skip them and the map's own page and any pasted
|
||||
snippet keep the old settings and refuse the new file. When the DX Life site's repo sits next to this one (`../doxa-life`), the full build also copies the maps into
|
||||
its `public/`. That is the one path set in `vite.doxa-maps-build-config.json` (`dropin.dest`); nothing else on
|
||||
your computer is searched. No site there: the step says so and skips. `DOXA_DROPIN=off` turns it off, and
|
||||
`bun run dropin` copies on demand.
|
||||
|
||||
**3. Embed a map.** Every map has its own embed script: its `embed.script` in `public/dx-maps/manifest.json` (an
|
||||
`embed.iframe` too). A snippet is two parts, the map's script and its tag:
|
||||
`embed.iframe` too). Paste it whole. It preloads the map engine, points the map at its data
|
||||
(`window.MAP_APP_API_URL`), loads the map's script with its integrity hash, and places the map's tag. The two
|
||||
lines at its core:
|
||||
|
||||
```html
|
||||
<script src="/dx-maps/doxa-simple-map/doxa-simple-map.js" integrity="sha384-…" crossorigin="anonymous"></script>
|
||||
|
|
@ -104,10 +108,10 @@ people groups of the world. When you work here, the goal is one of these:
|
|||
|
||||
| Goal | The production maps that serve it |
|
||||
|---|---|
|
||||
| **Reach a city** | Cities with Internet (`cities-internet`); Regions (`regions-dashboard`) and the country map (`country-map`) |
|
||||
| **Mobilize for people groups** | Big Picture (`dashboard-plus-grid`); Research Map + Grid (`research-map-plus-grid`) |
|
||||
| **Plan outreach** | Gospel Resources (`gospel-resources`): what exists in each language; Regions, by country |
|
||||
| **Pray: packets and prayer resources** | UPG-100 (`my-upg-100-list`): prayer packets; Prayer Progress (`doxa-simple-map`) |
|
||||
| **Reach a city** | Regions (`regions-dashboard`) and the country map (`country-map`): the people groups of each country. The production maps work by country and region; city-level planning (Prayer Walk Planner) is still experimental |
|
||||
| **Mobilize for people groups** | Big Picture (`dashboard-plus-grid`); Simple Research (`research-map-plus-grid`) |
|
||||
| **Plan outreach** | Gospel Resources (`gospel-resources`): what exists in each language; Cities with Internet (`cities-internet`): whether a group can be reached online |
|
||||
| **Pray: packets and prayer resources** | UPG 100 List (`my-upg-100-list`), Regions and Simple Research: posters and prayer packets; Prayer Progress (`doxa-simple-map`) |
|
||||
|
||||
- **Start with the skill:** [`skills/plugin-designer`](./skills/plugin-designer) teaches the patterns, the shared
|
||||
library and the checks.
|
||||
|
|
@ -115,7 +119,9 @@ people groups of the world. When you work here, the goal is one of these:
|
|||
every other plugin.
|
||||
- **Land on `development`**, one change at a time through the lock, with `bun run gates:pre` passing. `main` is
|
||||
production and moves only on a publish.
|
||||
- These 8 maps are in production. The experimental maps are frozen: leave them as they are.
|
||||
- These 8 maps are in production: the 7 in the gallery, plus the country map for DX Life's country pages. The
|
||||
other maps are experimental: leave them as they are unless your task is one
|
||||
of them.
|
||||
|
||||
*Click any ▸ to read more. Everything below is folded so you can scroll the pictures first.*
|
||||
|
||||
|
|
@ -124,17 +130,18 @@ people groups of the world. When you work here, the goal is one of these:
|
|||
## The 7 production maps
|
||||
|
||||
**Seven maps DX uses today** — each one a plugin you can embed on its own, and all seven together in
|
||||
the Research Gallery.
|
||||
the Research Gallery, which DX Life publishes as **Deep Research**. An eighth production map, the country map
|
||||
(`country-map`), draws DX Life's country pages; it isn't in the gallery.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/prod-big-picture.png" alt="Big Picture"><br><b>Big Picture</b><br><sub>the whole research dashboard: filters, map, charts, people groups</sub></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/prod-simple-research.png" alt="Simple Research"><br><b>Simple Research</b><br><sub>the research map with the people-groups grid, made for phones</sub></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/prod-regions.png" alt="Regions"><br><b>Regions</b><br><sub>every region and its countries, then a dashboard per region</sub></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/cap4-upg100-mfe.png" alt="UPG-100 List"><br><b>UPG-100 List</b><br><sub>prayer lists of 100 people groups, with posters to print</sub></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/cap2-prayer.png" alt="Prayer Progress"><br><b>Prayer Progress</b><br><sub>where prayer is happening, and where it is still needed</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/cap2-prayer.png" alt="Prayer Progress"><br><b>Prayer Progress</b><br><sub>where prayer is happening, and where it is still needed</sub></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/cap4-upg100-mfe.png" alt="UPG-100 List"><br><b>UPG-100 List</b><br><sub>prayer lists of 100 people groups, with posters to print</sub></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/prod-cities-internet.png" alt="Cities with Internet"><br><b>Cities with Internet</b><br><sub>internet access where unreached people groups live</sub></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/prod-gospel-resources.png" alt="Gospel Resources"><br><b>Gospel Resources</b><br><sub>which people groups have scripture, film or audio — and which have nothing</sub></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/cap5-doxa-maps-spa.png" alt="Research Gallery"><br><b>Research Gallery</b><br><sub>all seven in one app — capability 5</sub></td>
|
||||
|
|
@ -147,8 +154,8 @@ the Research Gallery.
|
|||
<tr>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/prod-simple-research-phone.png" alt="Simple Research on a phone"><br><b>Simple Research</b></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/prod-regions-phone.png" alt="Regions on a phone"><br><b>Regions</b></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/deploy2-mobile-upg100.png" alt="UPG-100 List on a phone"><br><b>UPG-100 List</b></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/prod-prayer-phone.png" alt="Prayer Progress on a phone"><br><b>Prayer Progress</b></td>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/deploy2-mobile-upg100.png" alt="UPG-100 List on a phone"><br><b>UPG-100 List</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="25%" valign="top"><img src="documentation/readme-images/prod-cities-internet-phone.png" alt="Cities with Internet on a phone"><br><b>Cities with Internet</b></td>
|
||||
|
|
@ -443,8 +450,9 @@ snippets in `manifest.json` stay relative (`./…`) and other sites can't paste
|
|||
the manifests in this repo's `public/`; `git restore public` puts them back after the copy.
|
||||
|
||||
This route is proven on DX Life: real pages rendering maps, one server, every production map passing
|
||||
its embed checks, copy-paste snippets included. Its drop-in branch copies the folder into `public/` and
|
||||
fills `/research` with the Research Gallery.
|
||||
its embed checks, copy-paste snippets included. Its drop-in branch copies the folder into `public/dx-maps`. Its
|
||||
`/research` page shows the research map by itself, with a link to `/deep-research`, a page not in the menu that
|
||||
holds the whole gallery as **Deep Research**.
|
||||
|
||||
**Embeds are pinned.** A host pastes a map's `embed.script` from `manifest.json`; its `integrity` hash
|
||||
makes the browser refuse the bundle if it changed by even one byte, so copy the snippet again after each
|
||||
|
|
@ -504,19 +512,22 @@ Every plugin's exact snippet is `embed.iframe` in `public/dx-maps/manifest.json`
|
|||
|
||||
</details>
|
||||
|
||||
##### JavaScript embed — the Research Gallery on dx-life ✅ production
|
||||
##### JavaScript embed — Deep Research for DX Life ✅ works today
|
||||
|
||||
**Two lines of code put a whole app inside your own page** — here, the Research Gallery on the
|
||||
dx-life research page.
|
||||
**Two lines of code put a whole app inside your own page** — here, the Research Gallery, which DX Life
|
||||
publishes as **Deep Research** on a page of its own (`/deep-research`).
|
||||
|
||||

|
||||

|
||||
|
||||
*An earlier build, when the gallery sat on DX Life's research page with its tabs in the old order. It now has a
|
||||
page of its own.*
|
||||
|
||||
<details>
|
||||
<summary><b>More about the JavaScript embed</b></summary>
|
||||
|
||||
Instead of a window onto another page, the map becomes **part of your page**, so your page can talk
|
||||
to it. On dx-life, one embed of the Research Gallery replaces the separate research map, list and
|
||||
regions dashboards. The exact snippet is `embed.script` in `manifest.json`; copy its `integrity`
|
||||
to it. On dx-life, one embed of the gallery is the whole `/deep-research` page, and the research page keeps
|
||||
the research map by itself. The exact snippet is `embed.script` in `manifest.json`; copy its `integrity`
|
||||
hash from there, because it changes with every build:
|
||||
|
||||
```html
|
||||
|
|
@ -677,8 +688,8 @@ your own page, set `"locale"` in its `profile-config`. Arabic reads right to lef
|
|||
|
||||
**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 `main` — the one
|
||||
official version — one at a time.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
|
@ -697,7 +708,7 @@ So the project works like a busy kitchen with one head chef:
|
|||
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 `main`. `main` always works.
|
||||
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.
|
||||
|
||||
|
|
@ -711,7 +722,8 @@ That's how many plugins get built at the same time, in one project, without the
|
|||
| Word | In plain terms |
|
||||
|---|---|
|
||||
| **repo** | the project folder, with its full history |
|
||||
| **main** | the one official version of the project that everyone builds from |
|
||||
| **development** | the shared working version: every agent's change is added here, one at a time |
|
||||
| **main** | the production version: it moves only when `development` is published |
|
||||
| **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 |
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 162 KiB |
|
|
@ -62,7 +62,10 @@ const VALUE_FLAGS = new Set(['--from', '--tree', '--protect', '--only'])
|
|||
const argOf = (k, d) => { const i = args.indexOf(k); const v = args[i + 1]; return i >= 0 && v !== undefined && !v.startsWith('--') ? v : d }
|
||||
// Destination: positional <dest> · else DOXA_DROPIN_DEST · else "dropin.dest" in the build-config
|
||||
// (the ONE place the host folder is declared; moving the drop-in = changing that line).
|
||||
const configDest = (() => { try { const d = JSON.parse(readFileSync(CONFIG, 'utf8')).dropin?.dest; return typeof d === 'string' && d.trim() ? resolve(ROOT, d.trim()) : null } catch { return null } })()
|
||||
// A config that EXISTS but does not parse is recorded, never swallowed: with --if-present a silent null would read
|
||||
// as "no destination configured" and a green build would never refresh the site.
|
||||
let configBroken = ''
|
||||
const configDest = (() => { try { const d = JSON.parse(readFileSync(CONFIG, 'utf8')).dropin?.dest; return typeof d === 'string' && d.trim() ? resolve(ROOT, d.trim()) : null } catch (e) { if (existsSync(CONFIG)) configBroken = e.message; return null } })()
|
||||
const DEST = args.find((a, i) => !a.startsWith('--') && !VALUE_FLAGS.has(args[i - 1])) || process.env.DOXA_DROPIN_DEST || configDest
|
||||
const FROM = resolve(ROOT, argOf('--from', 'public'))
|
||||
const DRY = args.includes('--dry-run')
|
||||
|
|
@ -86,6 +89,11 @@ const excludedInternal = (f) => EXCLUDE.has(f) || isDoc(f) || isStaging(f)
|
|||
const excluded = (f) => excludedInternal(f) || isNotShipped(f)
|
||||
|
||||
const usage = 'usage: node internal/sync-dropin.mjs [<dest>] [--from public] [--only a,b | --from-config] [--prune] [--protect a,b] [--dry-run] (<dest> defaults to $DOXA_DROPIN_DEST, then dropin.dest in vite.doxa-maps-build-config.json)'
|
||||
if (configBroken && (args.includes('--if-present') || args.includes('--from-config'))) {
|
||||
console.error(`[sync-dropin] build config unreadable: ${norm(relative(ROOT, CONFIG))}: ${configBroken}`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (!DEST && args.includes('--if-present')) { console.log('[sync-dropin] skipped: no drop-in destination configured (dropin.dest)'); process.exit(0) }
|
||||
if (!DEST) { console.error(`[sync-dropin] ${usage}`); process.exit(1) }
|
||||
const dest = resolve(DEST)
|
||||
// ★ --prune INTO A SITE'S WEB-ROOT WOULD DELETE THE SITE. --prune removes every file in <dest> outside the
|
||||
|
|
|
|||
|
|
@ -180,15 +180,23 @@ useShadowStyles(`
|
|||
.mi-btn:hover{ background:#f3f5fa; }
|
||||
.mi-btn:focus-visible{ outline:3px solid #2563eb; outline-offset:2px; }
|
||||
.mi-btn svg{ width:22px; height:22px; }
|
||||
.mi-at-right{ right:var(--map-info-side, 12px); }
|
||||
.mi-at-left{ left:var(--map-info-side, 12px); }
|
||||
/* LOGICAL SIDES, so an RTL page mirrors: "right" is the END of the line, which in Arabic is the LEFT.
|
||||
With a physical right:12px the button and pointer covered the first (active) tab of an Arabic tab
|
||||
bar, which starts at the right. */
|
||||
.mi-at-right{ inset-inline-end:var(--map-info-side, 12px); }
|
||||
.mi-at-left{ inset-inline-start:var(--map-info-side, 12px); }
|
||||
.mi-pointer{ position:absolute; z-index:20; top:calc(var(--map-info-top, 12px) + 4px);
|
||||
display:flex; align-items:center; gap:6px; pointer-events:none; white-space:nowrap;
|
||||
background:#1f2a44; color:#fff; font-size:13px; font-weight:600; line-height:1; padding:8px 12px;
|
||||
border-radius:999px; box-shadow:0 2px 10px rgba(0,0,0,.25); animation:mi-nudge 1.4s ease-in-out infinite; }
|
||||
.mi-pointer.mi-at-right{ right:calc(var(--map-info-side, 12px) + 50px); }
|
||||
.mi-pointer.mi-at-left{ left:calc(var(--map-info-side, 12px) + 50px); flex-direction:row-reverse;
|
||||
.mi-pointer.mi-at-right{ inset-inline-end:calc(var(--map-info-side, 12px) + 50px); }
|
||||
.mi-pointer.mi-at-left{ inset-inline-start:calc(var(--map-info-side, 12px) + 50px); flex-direction:row-reverse;
|
||||
animation-name:mi-nudge-left; }
|
||||
/* The flex order mirrors by itself under dir=rtl; the arrow glyph and the nudge do not, so flip both
|
||||
so the pointer still points and moves TOWARD the button. */
|
||||
.mi[dir="rtl"] .mi-pointer svg{ transform:scaleX(-1); }
|
||||
.mi[dir="rtl"] .mi-pointer.mi-at-right{ animation-name:mi-nudge-left; }
|
||||
.mi[dir="rtl"] .mi-pointer.mi-at-left{ animation-name:mi-nudge; }
|
||||
.mi-pointer svg{ width:16px; height:16px; flex:none; }
|
||||
@keyframes mi-nudge{ 0%,100%{ transform:translateX(0) } 50%{ transform:translateX(6px) } }
|
||||
@keyframes mi-nudge-left{ 0%,100%{ transform:translateX(0) } 50%{ transform:translateX(-6px) } }
|
||||
|
|
@ -218,7 +226,9 @@ useShadowStyles(`
|
|||
.mi-hit.mi-open{ cursor:zoom-out; }
|
||||
.mi-page{ max-width:420px; margin:0 auto; }
|
||||
.mi-pages .mi-page + .mi-page{ margin-top:12px; }
|
||||
.mi-stack{ position:relative; }
|
||||
/* isolation: the shadow card behind the first page sits at z-index:-1, which without its own stacking
|
||||
context is painted UNDER the dialog's white background, i.e. never seen. */
|
||||
.mi-stack{ position:relative; isolation:isolate; }
|
||||
.mi-stack::after{ content:''; position:absolute; inset:6px -6px -6px 6px; z-index:-1; border-radius:8px;
|
||||
background:#e3e6ee; box-shadow:0 1px 4px rgba(0,0,0,.15); }
|
||||
.mi-toggle{ display:inline-flex; align-items:center; min-height:44px; margin-top:10px; padding:0 16px;
|
||||
|
|
@ -242,6 +252,41 @@ useShadowStyles(`
|
|||
.mi-foot input{ width:20px; height:20px; }
|
||||
|
||||
/* A phone gets the whole screen: a floating card with margins leaves too little room for a page. */
|
||||
/* ★ THE ONE-TIME NUDGE IS DESKTOP-ONLY, AND THE THRESHOLD IS MEASURED, NOT CHOSEN. The pointer
|
||||
is anchored beside the button and extends ~213px INWARD, straight across whatever the tab puts
|
||||
in its top strip. Measured on Deep Research, on the shared browser, worst overlap per width:
|
||||
390 Regions "Region Dashboards" 1416px^2 UPG-100 1428px^2
|
||||
768 Cities legend 6560 Gospel legend 6560 + input 6240 Regions selector 4200
|
||||
900 Cities legend 2432 Gospel legend 2496 + input 2176 Regions selector 3696
|
||||
1024 Cities CLEAN Gospel CLEAN Regions selector 2100
|
||||
1200 CLEAN CLEAN CLEAN
|
||||
1280 CLEAN CLEAN CLEAN
|
||||
UPG-100 measures the same shape independently: 768 over "Prayer Resources" AND "Map", 1024 over
|
||||
"Map", clear at 1200/1280/1440 -- in en AND ar.
|
||||
So it is not a phone problem, which is how it was first reported and first fixed: it persists
|
||||
to 1024 and only clears near 1200. A max-width:600px rule would have left 768, 900 and 1024
|
||||
broken on four tabs -- found because gospel counted PANELS (.gm-legend/.gm-sheet) as well as
|
||||
controls, which a control-only probe misses entirely.
|
||||
ARABIC IS MEASURED, not argued: via DX Life's /ar/ route (dir=rtl confirmed), the pointer is
|
||||
hidden and clean at 390, 768 and 1024, and at 1440 it mirrors to the start side (x=104) and is
|
||||
clean there too.
|
||||
THE RULE IS ITS OWN BLOCK, deliberately: the 600px block below also full-screens the dialog,
|
||||
and this has nothing to do with the phone sheet. The .98 covers fractional viewport widths
|
||||
under browser zoom.
|
||||
Hiding it below the measured clear width is the only rule that holds everywhere: the button it
|
||||
points at is untouched, so nothing becomes unreachable, and a hint that covers a map's own
|
||||
legend, search box or view switcher is worse than no hint.
|
||||
Two other shapes were measured and both were worse: stacking it below the button traded one
|
||||
collision for five (Gospel's "Resources found" 2010px^2, plus the zoom +/- of three tabs), and
|
||||
hiding only its text still left 675px^2 on "Region Dashboards" and 900px^2 on Gospel's input.
|
||||
⚠ NOT A PLACEMENT CHANGE. The per-tab pos.narrow values in the shell's tabInfo are measured per
|
||||
tab (the buttons sit at x=8, 12, 294 and 346 across the seven tabs at 390); an inset-inline-*
|
||||
pinned here would win by specificity and silently flatten all of them. The button is
|
||||
placement's business; this rule is only about the hint. */
|
||||
@media (max-width:1199.98px){
|
||||
.mi-pointer{ display:none; }
|
||||
}
|
||||
|
||||
@media (max-width:600px){
|
||||
.mi-scrim{ padding:0; align-items:stretch; }
|
||||
.mi-dialog{ width:100%; max-height:none; height:100%; border-radius:0; }
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "كل الشعوب في عرض واحد. صفِّ حسب الصلاة أو الشعب أو المنطقة أو الدولة أو اللغة، فتضيق الخريطة والجدول والإجماليات معًا.",
|
||||
"takeaway": "خذ معك الصفوف التي صفّيتها في جدول بيانات."
|
||||
"takeaway": "الصفوف التي صفّيتها، في جدول بيانات."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "الخريطة إلى جانب القائمة. اختر شعبًا على الخريطة فتتبعه القائمة، وضيِّق القائمة فتتبعها الخريطة.",
|
||||
"takeaway": "خذ معك ملصقًا للخريطة كما تراها، جاهزًا للطباعة، وحزمة صلاة لتوزّعها.",
|
||||
"takeaway": "ملصق للخريطة كما تراها، جاهز للطباعة، وحزمة صلاة لتوزّعها.",
|
||||
"examplePacket": "مثال على حزمة الصلاة: قلب الشرق الأوسط"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "افتح دولة لرؤية مجموعاتها، ثم أنشئ ملصقًا وحزمة صلاة ومجموعة بطاقات للطباعة أو المشاركة.",
|
||||
"note": "تُنشأ الملصقات والحزم والبطاقات في متصفحك لكل دولة ما عدا الهند والصين (قريبًا)."
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "الشعب",
|
||||
"country": "الدولة",
|
||||
"region": "المنطقة",
|
||||
"population": "السكان"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Alle Völker in einer Ansicht. Filtern Sie nach Gebet, Volk, Region, Land oder Sprache – Karte, Tabelle und Summen werden gemeinsam eingegrenzt.",
|
||||
"takeaway": "Nehmen Sie die gefilterten Zeilen als Tabelle mit."
|
||||
"takeaway": "Die gefilterten Zeilen als Tabelle."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "Die Karte neben der Liste. Wählen Sie ein Volk auf der Karte, und die Liste folgt; grenzen Sie die Liste ein, und die Karte folgt.",
|
||||
"takeaway": "Nehmen Sie ein Poster der Karte, wie Sie sie sehen, druckfertig, und ein Gebetspaket zum Verteilen mit.",
|
||||
"takeaway": "Ein Poster der Karte, wie Sie sie sehen, druckfertig, und ein Gebetspaket zum Verteilen.",
|
||||
"examplePacket": "Beispiel-Gebetsheft: Kerngebiet Naher Osten"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "Öffnen Sie ein Land, um seine Volksgruppen zu sehen, und erzeugen Sie dann ein Poster, ein Gebetspaket und ein Kartenset zum Drucken oder Teilen.",
|
||||
"note": "Poster, Pakete und Karten entstehen in Ihrem Browser für jedes Land außer Indien und China (in Kürze)."
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "Volk",
|
||||
"country": "Land",
|
||||
"region": "Region",
|
||||
"population": "Bevölkerung"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Every people group in one view. Filter by prayer, people, region, country or language, and the map, the table and the totals all narrow together.",
|
||||
"takeaway": "Take away the rows you filtered to, as a spreadsheet."
|
||||
"takeaway": "The rows you filtered to, as a spreadsheet."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "The map beside the list. Pick a people group on the map and the list follows it; narrow the list and the map follows back.",
|
||||
"takeaway": "Take away a poster of the map as you see it, ready to print, and a prayer packet to hand out.",
|
||||
"takeaway": "A poster of the map as you see it, ready to print, and a prayer packet to hand out.",
|
||||
"examplePacket": "Example packet: Middle East Core"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "Open a country to see its groups, then generate a poster, a prayer packet and a card deck to print or share.",
|
||||
"note": "Posters, packets and cards generate in your browser for each country except India and China (coming soon)."
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "People Group",
|
||||
"country": "Country",
|
||||
"region": "Region",
|
||||
"population": "Population"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Todos los pueblos en una sola vista. Filtre por oración, pueblo, región, país o idioma: el mapa, la tabla y los totales se reducen a la vez.",
|
||||
"takeaway": "Llévese las filas que filtró, en una hoja de cálculo."
|
||||
"takeaway": "Las filas que filtró, en una hoja de cálculo."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "El mapa junto a la lista. Elija un pueblo en el mapa y la lista lo sigue; reduzca la lista y el mapa la sigue.",
|
||||
"takeaway": "Llévese un póster del mapa tal como lo ve, listo para imprimir, y un paquete de oración para repartir.",
|
||||
"takeaway": "Un póster del mapa tal como lo ve, listo para imprimir, y un paquete de oración para repartir.",
|
||||
"examplePacket": "Paquete de oración de ejemplo: núcleo de Oriente Medio"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "Abre un país para ver sus grupos y genera un póster, un paquete de oración y una baraja de tarjetas para imprimir o compartir.",
|
||||
"note": "Los pósteres, paquetes y tarjetas se generan en tu navegador para cada país excepto India y China (próximamente)."
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "Pueblo",
|
||||
"country": "País",
|
||||
"region": "Región",
|
||||
"population": "Población"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Tous les peuples dans une seule vue. Filtrez par prière, peuple, région, pays ou langue : la carte, le tableau et les totaux se réduisent ensemble.",
|
||||
"takeaway": "Emportez les lignes filtrées sous forme de tableur."
|
||||
"takeaway": "Les lignes filtrées, sous forme de tableur."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "La carte à côté de la liste. Choisissez un peuple sur la carte et la liste le suit ; réduisez la liste et la carte suit.",
|
||||
"takeaway": "Emportez une affiche de la carte telle que vous la voyez, prête à imprimer, et un dossier de prière à distribuer.",
|
||||
"takeaway": "Une affiche de la carte telle que vous la voyez, prête à imprimer, et un dossier de prière à distribuer.",
|
||||
"examplePacket": "Livret de prière d’exemple : cœur du Moyen-Orient"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "Ouvrez un pays pour voir ses groupes, puis générez une affiche, un dossier de prière et un jeu de cartes à imprimer ou à partager.",
|
||||
"note": "Affiches, dossiers et cartes sont générés dans votre navigateur pour chaque pays sauf l'Inde et la Chine (bientôt disponible)."
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "Peuple",
|
||||
"country": "Pays",
|
||||
"region": "Région",
|
||||
"population": "Population"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "सभी जनसमूह एक ही दृश्य में। प्रार्थना, जनसमूह, क्षेत्र, देश या भाषा से छाँटें — नक्शा, तालिका और कुल एक साथ सीमित होते हैं।",
|
||||
"takeaway": "छाँटी गई पंक्तियाँ स्प्रेडशीट के रूप में साथ ले जाएँ।"
|
||||
"takeaway": "छाँटी गई पंक्तियाँ, स्प्रेडशीट के रूप में।"
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "सूची के साथ नक्शा। नक्शे पर एक जनसमूह चुनें और सूची उसका अनुसरण करती है; सूची सीमित करें और नक्शा अनुसरण करता है।",
|
||||
"takeaway": "नक्शे का पोस्टर जैसा आप देखते हैं, छपाई के लिए तैयार, और बाँटने के लिए प्रार्थना पैकेट साथ ले जाएँ।",
|
||||
"takeaway": "नक्शे का पोस्टर, जैसा आप उसे देखते हैं, छपाई के लिए तैयार, और बाँटने के लिए प्रार्थना पैकेट।",
|
||||
"examplePacket": "उदाहरण प्रार्थना पुस्तिका: मध्य पूर्व का केंद्र"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "किसी देश को खोलकर उसके जनसमूह देखें, फिर छापने या साझा करने के लिए पोस्टर, प्रार्थना पैकेट और कार्ड सेट बनाएँ।",
|
||||
"note": "पोस्टर, पैकेट और कार्ड आपके ब्राउज़र में भारत और चीन को छोड़कर हर देश के लिए बनते हैं (शीघ्र आ रहा है)।"
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "जनसमूह",
|
||||
"country": "देश",
|
||||
"region": "क्षेत्र",
|
||||
"population": "जनसंख्या"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Tutti i popoli in un'unica vista. Filtra per preghiera, popolo, regione, paese o lingua: mappa, tabella e totali si restringono insieme.",
|
||||
"takeaway": "Porta con te le righe filtrate, come foglio di calcolo."
|
||||
"takeaway": "Le righe filtrate, come foglio di calcolo."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "La mappa accanto all'elenco. Scegli un popolo sulla mappa e l'elenco lo segue; restringi l'elenco e la mappa lo segue.",
|
||||
"takeaway": "Porta con te un poster della mappa così come la vedi, pronto da stampare, e un pacchetto di preghiera da distribuire.",
|
||||
"takeaway": "Un poster della mappa così come la vedi, pronto da stampare, e un pacchetto di preghiera da distribuire.",
|
||||
"examplePacket": "Libretto di preghiera di esempio: cuore del Medio Oriente"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "Apri un Paese per vederne i gruppi, poi genera un poster, un pacchetto di preghiera e un mazzo di schede da stampare o condividere.",
|
||||
"note": "Poster, pacchetti e schede vengono generati nel tuo browser per ogni Paese tranne India e Cina (prossimamente)."
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "Popolo",
|
||||
"country": "Paese",
|
||||
"region": "Regione",
|
||||
"population": "Popolazione"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@
|
|||
"takeaway": "Escritura, áudio, filme, vídeo bíblico, formação — e o que ainda falta. Clique num país para listar os seus povos, ou num povo para a sua ficha com ligações activas a cada recurso encontrado. «Nada encontrado» significa ausente dos acervos que esta versão consultou, nunca prova de que nada exista."
|
||||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Todos os povos numa só vista. Filtre por oração, povo, região, país ou idioma: o mapa, a tabela e os totais reduzem-se em conjunto.",
|
||||
"takeaway": "Leve as linhas que filtrou, como folha de cálculo."
|
||||
"lead": "Todos os povos em uma só vista. Filtre por oração, povo, região, país ou idioma, e o mapa, a tabela e os totais se restringem juntos.",
|
||||
"takeaway": "As linhas que você filtrou, em uma planilha."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "O mapa ao lado da lista. Escolha um povo no mapa e a lista segue-o; reduza a lista e o mapa segue.",
|
||||
"takeaway": "Leve um pôster do mapa tal como o vê, pronto a imprimir, e um pacote de oração para distribuir.",
|
||||
"lead": "O mapa ao lado da lista. Escolha um povo no mapa e a lista acompanha; restrinja a lista e o mapa acompanha também.",
|
||||
"takeaway": "Um pôster do mapa como você o vê, pronto para imprimir, e um pacote de oração para distribuir.",
|
||||
"examplePacket": "Pacote de oração de exemplo: núcleo do Oriente Médio"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "Abra um país para ver os seus grupos e gere um cartaz, um pacote de oração e um baralho de cartões para imprimir ou partilhar.",
|
||||
"note": "Cartazes, pacotes e cartões são gerados no seu navegador para cada país exceto a Índia e a China (em breve)."
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "Povo",
|
||||
"country": "País",
|
||||
"region": "Região",
|
||||
"population": "População"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Toate popoarele într-o singură vedere. Filtrează după rugăciune, popor, regiune, țară sau limbă — harta, tabelul și totalurile se restrâng împreună.",
|
||||
"takeaway": "Ia cu tine rândurile filtrate, ca foaie de calcul."
|
||||
"takeaway": "Rândurile filtrate, ca foaie de calcul."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "Harta lângă listă. Alege un popor pe hartă și lista îl urmează; restrânge lista și harta o urmează.",
|
||||
"takeaway": "Ia cu tine un poster al hărții așa cum o vezi, gata de tipărit, și un pachet de rugăciune de împărțit.",
|
||||
"takeaway": "Un poster al hărții așa cum o vezi, gata de tipărit, și un pachet de rugăciune de împărțit.",
|
||||
"examplePacket": "Pachet de rugăciune exemplu: nucleul Orientului Mijlociu"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "Deschide o țară pentru a-i vedea grupurile, apoi generează un afiș, un pachet de rugăciune și un set de cartonașe pentru tipărire sau partajare.",
|
||||
"note": "Afișele, pachetele și cartonașele se generează în browserul tău pentru fiecare țară în afară de India și China (în curând)."
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "Popor",
|
||||
"country": "Țară",
|
||||
"region": "Regiune",
|
||||
"population": "Populație"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "Все народы в одном представлении. Фильтруйте по молитве, народу, региону, стране или языку — карта, таблица и итоги сужаются вместе.",
|
||||
"takeaway": "Заберите отфильтрованные строки в виде таблицы."
|
||||
"takeaway": "Отфильтрованные строки в виде таблицы."
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "Карта рядом со списком. Выберите народ на карте — список последует за ним; сузьте список — карта последует за ним.",
|
||||
"takeaway": "Заберите плакат карты в том виде, как вы её видите, готовый к печати, и молитвенный пакет для раздачи.",
|
||||
"takeaway": "Плакат карты в том виде, как вы её видите, готовый к печати, и молитвенный пакет для раздачи.",
|
||||
"examplePacket": "Пример молитвенного буклета: центральная часть Ближнего Востока"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "Откройте страну, чтобы увидеть её народности, затем создайте плакат, молитвенный пакет и набор карточек для печати или отправки.",
|
||||
"note": "Плакаты, пакеты и карточки создаются в вашем браузере для каждой страны, кроме Индии и Китая (скоро)."
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "Народ",
|
||||
"country": "Страна",
|
||||
"region": "Регион",
|
||||
"population": "Население"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
},
|
||||
"dashboard-plus-grid": {
|
||||
"lead": "在一个视图中查看所有民族。按祷告、民族、区域、国家或语言筛选,地图、表格和总计会一同收窄。",
|
||||
"takeaway": "把筛选后的行作为电子表格带走。"
|
||||
"takeaway": "筛选后的行,作为电子表格。"
|
||||
},
|
||||
"research-map-plus-grid": {
|
||||
"lead": "地图与列表并排。在地图上选择一个民族,列表随之跟随;缩小列表范围,地图也随之跟随。",
|
||||
"takeaway": "带走一张你所见地图的海报,可直接打印,以及一份可分发的祷告手册。",
|
||||
"takeaway": "一张你所见地图的海报,可直接打印,以及一份可分发的祷告手册。",
|
||||
"examplePacket": "示例祷告手册:中东核心地区"
|
||||
},
|
||||
"prayer-map": {
|
||||
|
|
@ -47,5 +47,11 @@
|
|||
"takeaway": "打开一个国家查看其族群,然后生成海报、祷告册和卡片集,可打印或分享。",
|
||||
"note": "海报、祷告册和卡片在您的浏览器中生成,适用于除印度和中国以外的每个国家(即将推出)。"
|
||||
}
|
||||
},
|
||||
"col": {
|
||||
"peopleGroup": "民族",
|
||||
"country": "国家",
|
||||
"region": "区域",
|
||||
"population": "人口"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,13 +143,17 @@ export function createCardRenderer({ strings, lang = 'en', stock = 'poker',
|
|||
? `left:10px;width:${COL - 20}px;top:${12 + PH + 6}px;bottom:10px;align-items:flex-start;justify-content:center;text-align:center;`
|
||||
: `left:${12 + PW + 10}px;right:8px;top:0;height:${BAND}px;`}
|
||||
font:700 ${SIDE ? 14 : 17}px Georgia,serif;color:#1b1a17;line-height:1.1;overflow:hidden;
|
||||
overflow-wrap:anywhere;-webkit-hyphens:auto;hyphens:auto}
|
||||
overflow-wrap:anywhere}
|
||||
/* Steps by the LONGEST WORD as well as the length: the name sits in a narrow box beside the photo,
|
||||
and one unbreakable word (the dataset has 16-letter ones, e.g. Pagativeshagallu) would overflow it
|
||||
sideways. overflow-wrap:anywhere is the last resort, so a name is never cut off without a sign;
|
||||
hyphens:auto (the deck's <html lang> names the language) puts a hyphen where it does break. */
|
||||
sideways. overflow-wrap:anywhere is the last resort, so a name is never cut off without a sign. */
|
||||
.nm.long{font-size:${SIDE ? 11.5 : 14}px}
|
||||
.nm.xlong{font-size:${SIDE ? 10.5 : 11}px}
|
||||
/* A HYPHEN ONLY WHERE A WORD CANNOT FIT. hyphens:auto on every name would let the browser split ANY
|
||||
word at a line end ("Northern Conchu-/cos"), and a people group's name split mid-word is worse than
|
||||
the problem it solves. So it applies only to names holding a 14+ letter word (12 of 2,107), and the
|
||||
limit keeps short words whole: the only multi-word ones are "Deaf <X>", and Deaf is never split. */
|
||||
.nm.hy{-webkit-hyphens:auto;hyphens:auto;hyphenate-limit-chars:10 4 4}
|
||||
|
||||
.body{position:absolute;right:14px;overflow:hidden;
|
||||
left:${SIDE ? COL + 12 : 14}px;
|
||||
|
|
@ -177,7 +181,8 @@ export function createCardRenderer({ strings, lang = 'en', stock = 'poker',
|
|||
// The name's size step, from its length and its longest word (see .nm above).
|
||||
function nameSize(name) {
|
||||
const word = Math.max(0, ...String(name).split(/[\s,()/-]+/).map((w) => w.length));
|
||||
if (word >= 14 || name.length > 28) return ' xlong';
|
||||
if (word >= 14) return ' xlong hy';
|
||||
if (name.length > 28) return ' xlong';
|
||||
if (word >= 11 || name.length > 16) return ' long';
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
|||
const PRAY_CELL = 84;
|
||||
const SET_PANEL = 660; // one square sets panel: two + a 40px gap fit 1380px
|
||||
const SET_SOLO = 960; // one chart per page: a large square, centred
|
||||
// ★ MEASURED FILL, 2026-09-29: at 960px the single-chart pages ended 60-69% of the way down the sheet
|
||||
// (525-702px of blank paper below). The chart is an SVG, so it scales: the bounded set has nothing
|
||||
// under it and takes nearly the full 1380px row; the centered set keeps room for its ring legend.
|
||||
const SET_SOLO_W = { bounded: 1320, centered: 1240 };
|
||||
const PRAY_W = PRAY_CELL * PRAY_COLS.length; // 252px
|
||||
// What is left is the description — the widest column on the page, and the point of the table.
|
||||
// For comparison an ENTIRE card in the old 2x10 grid was 683px wide; the description alone is 50%
|
||||
|
|
@ -194,9 +198,8 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
|||
.lc-wk{height:16px;margin-bottom:4px;font:400 10px Georgia,serif;color:#6f6a5c}
|
||||
.lc-wk span{text-align:center;line-height:16px}
|
||||
.lc-dec{grid-auto-rows:11px;row-gap:3px}
|
||||
.lc-plain{width:auto}
|
||||
.lc-plain .lc-all{display:grid;grid-template-columns:repeat(80,1fr);gap:3px}
|
||||
.lc-plain .lc-c{width:auto;height:auto;aspect-ratio:1}
|
||||
.lc-plain .lc-all{display:grid;grid-template-columns:repeat(65,14.5px);gap:3px}
|
||||
.lc-plain .lc-c{width:14.5px;height:14.5px}
|
||||
.lc-plain + .lc-legend{font-size:22px;gap:48px}
|
||||
.lc-plain ~ .lc-per{font-size:20px}
|
||||
.lc-dec+.lc-dec{margin-top:6px;padding-top:6px;border-top:1px solid #d8d3c4}
|
||||
|
|
@ -261,6 +264,9 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
|||
/* The legend names EVERY ring, numbered as the figure numbers them, so a reader can read the scale
|
||||
from the same dialect outward to the language family — including rings this list leaves empty. */
|
||||
.set-row-solo .set-panel{width:${SET_SOLO}px}
|
||||
.set-solo-bounded .set-panel{width:${SET_SOLO_W.bounded}px}
|
||||
.set-solo-centered .set-panel{width:${SET_SOLO_W.centered}px}
|
||||
.set-solo-centered + .set-legend, .set-solo-centered ~ .set-legend{max-width:${SET_SOLO_W.centered}px}
|
||||
.set-legend{margin:26px auto 0;max-width:${SET_SOLO}px}
|
||||
.set-legend-h{font:700 17px Georgia,serif;color:#1b1a17;margin:0 0 8px}
|
||||
.set-rings{margin:0;padding:0;list-style:none;display:grid;grid-template-columns:1fr 1fr;column-gap:40px;
|
||||
|
|
@ -889,8 +895,10 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
|||
const head = axes ? `<div class="lc-wk">${WEEK_MARKS.map((w) => `<span style="grid-column:${w + 1}">${w}</span>`).join('')}</div>` : '';
|
||||
// ★ WITHOUT AXES THE GRID REFLOWS TO FILL THE PAGE. 52 columns exist only because a year has 52
|
||||
// weeks; a page whose box is a share of PEOPLE has no such constraint, and at 52 x 11px the grid
|
||||
// used about half the sheet's width. The same 5,200 boxes are laid out 80 x 65 (both divide 5,200,
|
||||
// so the rectangle is whole), the columns stretch to the frame, and the cells stay square.
|
||||
// used about half the sheet's width. The same 5,200 boxes are laid out 65 x 80 (both divide 5,200,
|
||||
// so the rectangle is whole) in 14.5px squares. MEASURED in a browser: 80 x 65 across the full width
|
||||
// left 478px of blank sheet below the legend (72% of the height used), because a wide grid is short;
|
||||
// 65 x 80 spends that height on bigger boxes and still leaves room for a longer lede in other languages.
|
||||
if (!axes) {
|
||||
const cells = []
|
||||
for (let i = 0; i < TOTAL; i++) cells.push(`<i class="lc-c${i < n ? ' ' + shadeClass : ''}"></i>`)
|
||||
|
|
@ -1342,7 +1350,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
|||
<div class="pg-head pg-head-c"><div class="pg-kicker">${esc(t('sets_kicker'))}</div>
|
||||
<h1 class="pg-h1">${esc(t('sets_title'))}</h1></div>
|
||||
${i === 0 ? `<p class="pg-lede pg-lede-c">${esc(t('sets_lede'))}</p>` : ''}
|
||||
<div class="set-row set-row-solo">${p.html}</div>
|
||||
<div class="set-row set-row-solo set-solo-${p.kind}">${p.html}</div>
|
||||
${p.kind === 'centered' ? legend : ''}
|
||||
`, `sheet-sets sheet-flow sheet-sets-${p.kind}`)).join('');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,17 +105,40 @@ const heading = computed(() => config.value.title
|
|||
//
|
||||
// Set on the HOST rather than on .bp-root, deliberately: `dir` takes effect through the inherited
|
||||
// CSS `direction`, and inheritance crosses shadow boundaries — so one attribute covers this
|
||||
// element's own chrome AND both nested elements beneath it. Only ever SET, never cleared: a host
|
||||
// element's own chrome AND both nested elements beneath it. Set and CLEARED, but only ever ours: a host
|
||||
// page that already declares its own dir owns that decision.
|
||||
const textDir = computed(() => {
|
||||
const l = hostLang.value || getRuntimeLocale() || detectLocale()
|
||||
return RTL_LOCALES.has(l) ? 'rtl' : 'ltr'
|
||||
})
|
||||
const _self = getCurrentInstance()
|
||||
// ★ WE CLEAR ONLY THE DIRECTION WE SET. The line below used to be a one-way
|
||||
// `if (rtl) setAttribute('dir','rtl')` with no else, so a reader who switched from an RTL locale to
|
||||
// an LTR one (ar -> zh) kept a right-to-left layout for the rest of the session: Chinese laid out
|
||||
// backwards, which reads as a broken page rather than a stale setting.
|
||||
// The original reason for never clearing is still honoured, and is why this remembers rather than
|
||||
// just removing: a host page that declares its OWN dir owns that decision, so the value found before
|
||||
// we first wrote is restored verbatim, and removed only when there was none.
|
||||
// ★ CAPTURED AT MOUNT, NOT AT FIRST WRITE. The capture used to sit inside the rtl branch, so it
|
||||
// recorded whatever `dir` happened to be on the host the first time WE wrote — and if anything else
|
||||
// had already set rtl by then, that value was recorded as "the page's own" and faithfully restored
|
||||
// forever after. MEASURED on the Big Picture leaf: ar left rtl, and zh put rtl straight back.
|
||||
// Reading it on the FIRST call instead, before any write of ours, is the only moment the attribute
|
||||
// is certainly the page's and not ours.
|
||||
let _hostDir = null
|
||||
let _weSetDir = false
|
||||
function reflectDir() {
|
||||
try {
|
||||
const host = _self?.proxy?.$el?.getRootNode?.()?.host
|
||||
if (host && textDir.value === 'rtl') host.setAttribute('dir', 'rtl')
|
||||
if (!host || !host.setAttribute) return
|
||||
if (_hostDir === null) _hostDir = host.getAttribute('dir') || ''
|
||||
if (textDir.value === 'rtl') {
|
||||
host.setAttribute('dir', 'rtl')
|
||||
_weSetDir = true
|
||||
} else if (_weSetDir) {
|
||||
if (_hostDir) host.setAttribute('dir', _hostDir); else host.removeAttribute('dir')
|
||||
_weSetDir = false
|
||||
}
|
||||
} catch (_) { /* no host (SSR/tests) — nothing to reflect */ }
|
||||
}
|
||||
onMounted(reflectDir)
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ watchEffect(() => { if (hostLang.value) setRuntimeLocale(hostLang.value) })
|
|||
// Set on the custom-element HOST rather than on the layout inside it, deliberately: `dir` takes
|
||||
// effect through the inherited CSS `direction`, and inheritance crosses shadow boundaries — so one
|
||||
// attribute on the host covers this composite's own chrome AND every nested element beneath it,
|
||||
// which is the half the audit found missing. Only ever SET, never cleared: a host page that already
|
||||
// which is the half the audit found missing. Set and CLEARED, but only ever ours: a host page that already
|
||||
// declares its own dir owns that decision.
|
||||
const textDir = computed(() => {
|
||||
const l = hostLang.value || getRuntimeLocale() || detectLocale()
|
||||
|
|
@ -316,10 +316,29 @@ const textDir = computed(() => {
|
|||
// it is". Measured in this tree before, under Vue 3.5.42, when research-map.vue's emitFilterState
|
||||
// died the same way; the fix is its _selfInstance, and this is the same fix.
|
||||
const _selfInstance = getCurrentInstance()
|
||||
// ★ WE CLEAR ONLY THE DIRECTION WE SET. This was a one-way `if (rtl) setAttribute` with no else,
|
||||
// so ar -> zh kept a right-to-left layout for the rest of the session. The host page's own dir is
|
||||
// still respected: the value found before we first wrote is restored verbatim.
|
||||
// ★ CAPTURED AT MOUNT, NOT AT FIRST WRITE. The capture used to sit inside the rtl branch, so it
|
||||
// recorded whatever `dir` happened to be on the host the first time WE wrote — and if anything else
|
||||
// had already set rtl by then, that value was recorded as "the page's own" and faithfully restored
|
||||
// forever after. MEASURED on the Big Picture leaf: ar left rtl, and zh put rtl straight back.
|
||||
// Reading it on the FIRST call instead, before any write of ours, is the only moment the attribute
|
||||
// is certainly the page's and not ours.
|
||||
let _hostDir = null
|
||||
let _weSetDir = false
|
||||
function reflectDir() {
|
||||
try {
|
||||
const host = _selfInstance?.proxy?.$el?.getRootNode?.()?.host
|
||||
if (host && textDir.value === 'rtl') host.setAttribute('dir', 'rtl')
|
||||
if (!host || !host.setAttribute) return
|
||||
if (_hostDir === null) _hostDir = host.getAttribute('dir') || ''
|
||||
if (textDir.value === 'rtl') {
|
||||
host.setAttribute('dir', 'rtl')
|
||||
_weSetDir = true
|
||||
} else if (_weSetDir) {
|
||||
if (_hostDir) host.setAttribute('dir', _hostDir); else host.removeAttribute('dir')
|
||||
_weSetDir = false
|
||||
}
|
||||
} catch (_) { /* no host (SSR/tests) — nothing to reflect */ }
|
||||
}
|
||||
onMounted(reflectDir)
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@
|
|||
<div class="dms-slot" :ref="el => setSlot(i, el)"></div>
|
||||
<!-- The tab's "what can this do for you?" button (library MapInfo), from src/tabInfo.js. It sits
|
||||
over the pane, top-right; profile-config info:false removes it. -->
|
||||
<MapInfo v-if="infoFor(t)" :key="'info-' + t.id" v-bind="infoFor(t)"
|
||||
<MapInfo v-if="infoFor(t)" :key="'info-' + t.id" v-bind="infoProps(t)" :style="infoStyle(t)" :placement="infoPos(t).placement || 'top-right'"
|
||||
:assets-base="assetsBase" :locale="effectiveLocale" />
|
||||
<div v-if="t.state === 'loading'" class="dms-hint">{{ tr('loading', { label: tabLabel(t) }) }}</div>
|
||||
<div v-else-if="t.state === 'error'" class="dms-hint err">
|
||||
|
|
@ -1104,6 +1104,22 @@ export default defineComponent({
|
|||
// A tab's button shows only when its lead text is in the bundled catalogue: a key that has not
|
||||
// landed would otherwise open a popup of raw key names. English is the check because every
|
||||
// locale falls back to it.
|
||||
// The entry minus `pos`, which is placement for the button, not a MapInfo prop.
|
||||
infoProps(t) { const { pos, ...props } = this.infoFor(t) || {}; return props },
|
||||
// pos -> MapInfo's own offset variables, inherited by its button and pointer.
|
||||
// The tab's placement at this width: pos, with pos.narrow laid over it on a phone.
|
||||
infoPos(t) {
|
||||
const pos = (this.infoFor(t) || {}).pos || {}
|
||||
const { narrow, ...wide } = pos
|
||||
return this.narrow && narrow ? { ...wide, ...narrow } : wide
|
||||
},
|
||||
infoStyle(t) {
|
||||
const pos = this.infoPos(t)
|
||||
const s = {}
|
||||
if (pos.top) s['--map-info-top'] = pos.top
|
||||
if (pos.side) s['--map-info-side'] = pos.side
|
||||
return s
|
||||
},
|
||||
infoFor(t) {
|
||||
const e = this.showInfo && t && TAB_INFO[t.id]
|
||||
return e && (!e.leadKey || tStatic(e.leadKey, 'en')) ? e : null
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@
|
|||
* map's WHOLE catalogue, in eleven locales, into the gallery bundle. upg100 owns 'mapInfo' and merges
|
||||
* every tab's translated strings into it. Name keys LITERALLY here: that is what the area scan reads.
|
||||
*
|
||||
* ★ pos PLACES THE BUTTON CLEAR OF THE MAP'S OWN CONTROLS (--map-info-top / --map-info-side, default
|
||||
* 12px each). The button sits at the END of the line (right in English, left in Arabic, see MapInfo),
|
||||
* which is also where most of these maps keep their zoom column; measured on rega's 1440 captures in
|
||||
* en and ar (2026-09-29), 12px covered the zoom-in button on Prayer Progress, Cities and Gospel, Big
|
||||
* Picture's header buttons and Simple Research's tab bar. side 56px sits beside a zoom column; top
|
||||
* 46px / 56px clears Big Picture's 38px header bar / Simple Research's tab bar. Regions and UPG-100
|
||||
* open on a view-switch strip whose END holds their Share button, which the default covered exactly
|
||||
* (found by geometry, not visible in a screenshot: only Share's ring showed around the button).
|
||||
* pos.narrow overrides on a phone (the shell's `narrow`, max-width:760px), where the chrome differs:
|
||||
* Regions' and UPG-100's strip is full at its END (selector + Share) and empty at its START, so the
|
||||
* button goes to the start (placement 'top-left', which MapInfo resolves to the logical start, so
|
||||
* Arabic mirrors); Gospel's panel spans to 345px of 390, so its button takes the corner above the
|
||||
* zoom column. Measured with the same probe at 390 in en and ar.
|
||||
* ★ ADD AN EXAMPLE ONLY WHEN ITS FILE IS IN THE TREE. A missing file renders the popup's "not
|
||||
* available" box in front of the audience; an example that is not there yet is simply left out.
|
||||
* ★ KEYS MUST NOT CONTAIN A DOT beyond the area separator: the gallery has no vue-i18n, so MapInfo
|
||||
|
|
@ -19,6 +32,7 @@
|
|||
export const TAB_INFO = {
|
||||
'upg-100': {
|
||||
storageKey: 'upg-100',
|
||||
pos: { side: '56px', narrow: { placement: 'top-left', side: '12px' } },
|
||||
leadKey: 'mapInfo.tab.upg-100.lead',
|
||||
takeawayKey: 'mapInfo.tab.upg-100.takeaway',
|
||||
// The packet pages (12) and the card deck (70) join this list when their pre-generated shots land
|
||||
|
|
@ -29,6 +43,7 @@ export const TAB_INFO = {
|
|||
},
|
||||
'regions-dashboard': {
|
||||
storageKey: 'regions-dashboard',
|
||||
pos: { side: '56px', narrow: { placement: 'top-left', side: '8px' } },
|
||||
leadKey: 'mapInfo.tab.regions-dashboard.lead',
|
||||
takeawayKey: 'mapInfo.tab.regions-dashboard.takeaway',
|
||||
// "except India and China": India is genuinely unsupported -- no sheet, no geometry, and 855
|
||||
|
|
@ -43,16 +58,19 @@ export const TAB_INFO = {
|
|||
},
|
||||
'dashboard-plus-grid': {
|
||||
storageKey: 'dashboard-plus-grid',
|
||||
pos: { top: '46px' },
|
||||
leadKey: 'mapInfo.tab.dashboard-plus-grid.lead',
|
||||
takeawayKey: 'mapInfo.tab.dashboard-plus-grid.takeaway',
|
||||
// Big Picture's export is a spreadsheet, so its example is a real excerpt of one (sr): the six
|
||||
// largest groups by population among those joinable locally, of the full list's 2,107. The column
|
||||
// keys are the ones the live table resolves, so the popup header and the real one cannot drift;
|
||||
// cell text stays untranslated because the live table's cells are not translated either.
|
||||
// headers are the live table's own words, COPIED verbatim from dashboard.json into mapInfo.col (all
|
||||
// eleven locales): naming dashboard.* here pulled the whole 'dashboard' area into this bundle
|
||||
// (+50 KB raw). If the table's headers are reworded, copy them again. Cell text stays untranslated
|
||||
// because the live table's cells are not translated either.
|
||||
examples: [{
|
||||
kind: 'table',
|
||||
columns: [{ key: 'dashboard.people_group' }, { key: 'dashboard.country' },
|
||||
{ key: 'dashboard.region' }, { key: 'dashboard.population' }],
|
||||
columns: [{ key: 'mapInfo.col.peopleGroup' }, { key: 'mapInfo.col.country' },
|
||||
{ key: 'mapInfo.col.region' }, { key: 'mapInfo.col.population' }],
|
||||
rows: [
|
||||
['Hararghe Oromo', 'Ethiopia', 'Africa', 6355000],
|
||||
['Gaaliin', 'Sudan', 'Middle East', 4520000],
|
||||
|
|
@ -66,24 +84,28 @@ export const TAB_INFO = {
|
|||
},
|
||||
'research-map-plus-grid': {
|
||||
storageKey: 'research-map-plus-grid',
|
||||
pos: { top: '56px', side: '56px' },
|
||||
leadKey: 'mapInfo.tab.research-map-plus-grid.lead',
|
||||
takeawayKey: 'mapInfo.tab.research-map-plus-grid.takeaway',
|
||||
examples: [],
|
||||
},
|
||||
'prayer-map': {
|
||||
storageKey: 'prayer-map',
|
||||
pos: { side: '56px' },
|
||||
leadKey: 'mapInfo.tab.prayer-map.lead',
|
||||
takeawayKey: 'mapInfo.tab.prayer-map.takeaway',
|
||||
examples: [],
|
||||
},
|
||||
'cities-internet': {
|
||||
storageKey: 'cities-internet',
|
||||
pos: { side: '56px' },
|
||||
leadKey: 'mapInfo.tab.cities-internet.lead',
|
||||
takeawayKey: 'mapInfo.tab.cities-internet.takeaway',
|
||||
examples: [],
|
||||
},
|
||||
'gospel-resources': {
|
||||
storageKey: 'gospel-resources',
|
||||
pos: { side: '56px', narrow: { side: '4px' } },
|
||||
leadKey: 'mapInfo.tab.gospel-resources.lead',
|
||||
takeawayKey: 'mapInfo.tab.gospel-resources.takeaway',
|
||||
examples: [],
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 83 KiB |