This commit is contained in:
Doxa Maps 2026-10-01 00:56:01 +00:00
commit 5a4611d291
19 changed files with 42 additions and 44 deletions

View file

@ -188,7 +188,7 @@ useShadowStyles(
'grid-template-columns:var(--facet-w,264px) minmax(0,1fr) var(--rail-w,360px);' +
'grid-template-rows:auto auto auto minmax(0,1fr) var(--table-h,38%);' +
// The charts rail runs the full height beside the map AND the table: sized to the map row alone it was
// ~300px and clipped its four charts (rega: the charts did not look good).
// ~300px and clipped its four charts.
'grid-template-areas:"top top top" "combos combos combos" "note note note" "facets map rail" "facets grid rail";' +
'font:13px/1.35 Roboto,-apple-system,"Segoe UI",sans-serif;overflow:hidden}' +
'.gb-top{grid-area:top;display:flex;align-items:center;flex-wrap:wrap;gap:10px 16px;padding:10px 14px;background:#fff;border-bottom:1px solid #d1d4cd}' +

View file

@ -9,7 +9,7 @@
* data/tables/doxa-people-groups.json (gospel-plugin) slug, lat/lng, pray link
* data/tables/country-connectivity.json (World Bank) internet %, year, mobile per 100
* data/tables/internet-by-country.json (cities-internet) top platform
* data/tables/internet-by-adm1.json (exp-shepherd) India by state, NFHS-5
* data/tables/internet-by-adm1.json (cities-internet) India by state, NFHS-5
* data/tables/messaging-channels.json (cities-internet) top free apps per country (Apple, iOS only)
* data/tables/social-platforms.json (cities-internet) social referral share per country (StatCounter)
* data/tables/gospel-by-province-groups.json (gospel-plugin) each IND/CHN group's province (approximate)

View file

@ -7,7 +7,7 @@
* This one server-renders them (Vue's renderer, through vite's module loader, no browser, no WebGL):
* every panel's setup, computeds and template run against the real table, tree and resources, and the
* HTML is asserted on CONTENT the panel must show, never on the panel's own count of itself.
* onMounted does not run under SSR, so map layers are not covered here; that is the browser's (rega's).
* onMounted does not run under SSR, so map layers are not covered here; that is the browser check's job.
*
* `--poison` blanks the rows handed to the panels and requires the content assertions to FAIL.
* Exit 0 pass · 1 fail · 75 not measured (vite could not load the modules).

View file

@ -13,7 +13,7 @@
* (Partial Scripture comes from eBible's index, already in data/tables, and is read by the builder.)
*
* Codes are mapped EXPLICITLY and every code that cannot be mapped is COUNTED and listed, never dropped
* silently (dx-shepherd's guard 3).
* silently.
* Writes data/translation-readiness/snapshot.json. Refuses a much smaller snapshot than the last one.
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'

View file

@ -44,7 +44,7 @@ const R = 120, SPREAD = 260
// ★ 0.095, NOT THE 0.17 DEFAULT — the same clamp as the centered chart, and the same number so the
// two sets read at one scale. renderWorldCopies:false stops the engine zooming out past
// log2(canvasWidth/512), so a wide, short figure cannot fit its height at 0.17 and the outermost
// groups fall outside the frame (rega, 1440: 54 outside in ar and 97 in en — the width, not RTL).
// groups fall outside the frame (measured at 1440: 54 outside in ar and 97 in en — the width, not RTL).
// Kept EQUAL to GiCenteredSetMap's on purpose: a reader switching tabs compares two drawings, and a
// different footprint per tab would make the same population look like a different size.
const EXTENT_FRACTION = 0.095

View file

@ -134,7 +134,7 @@ const fmt = (n) => formatCount(Number(n) || 0)
useShadowStyles(
// ROW, NOT COLUMN, and overflow hidden. Stacked in a centred column the legend — a 360px card
// with a 3x3 grid of 44px cells and four footer lines — is taller than the pane on its own, so
// the chart was pushed out of view entirely and the whole panel overflowed its host: rega saw
// the chart was pushed out of view entirely and the whole panel overflowed its host: it showed
// the legend covering the row below and the caption drawn over the page tabs.
// Every child gets min-height/min-width 0, because a flex item's default minimum is its CONTENT
// size — without it an item never shrinks below its natural height and the container overflows
@ -148,10 +148,10 @@ useShadowStyles(
// had this; the centered one did not, which is the kind of difference two files drift into
// when the same rule is written twice.
// min-height in the narrow column: a flex child with auto content can resolve to zero height,
// which is what a 0x0 chart is. rega measured exactly that at 390px.
// which is what a 0x0 chart is, as measured at 390px.
// flex-basis 0, not auto. The map inside is position:absolute, so the figure has NO intrinsic
// width to be sized from — with basis auto it asks for its content, the content is nothing, and
// the legend beside it takes the column. rega measured the figure at 39px across while the
// the legend beside it takes the column. The figure measured 39px across while the
// decoder card had the rest. Growing from zero makes the figure take the remaining space
// whatever it happens to contain.
// min-width is a FLOOR, not 0. The shipped CSS was right and the figure was still 39px wide:

View file

@ -52,7 +52,7 @@ const EXTENT = 30 + (RING_MAX - 1) * 22 // 228 — the ring the outermo
// ★ 0.095, NOT THE 0.17 DEFAULT. With renderWorldCopies:false the engine will not zoom out past the
// point where ONE WORLD fills the canvas WIDTH, so the minimum zoom is log2(canvasWidth/512) and a
// wide, short figure is clamped too close to fit its own height. minZoom is irrelevant; the width
// constraint wins. rega measured the symptom at 1440 — 54 groups outside the frame in ar, 97 in en,
// constraint wins. The symptom measured at 1440 — 54 groups outside the frame in ar, 97 in en,
// so it was never RTL-specific. At 0.17 the drawing spans 0.34 of the Mercator square and needs
// 271px of vertical room where the canvas offers 249; at 0.095 it spans 0.17 and needs 151px
// against 169 available at padding 40. Checked by check:gospel-set-fit.

View file

@ -85,7 +85,7 @@ function onBar(row) {
useShadowStyles(
'.gic{display:flex;flex-direction:column;gap:4px;height:100%;overflow-y:auto;box-sizing:border-box;padding:10px 12px 16px;background:#fff;color:#3b463d;font:13px/1.35 Roboto,-apple-system,"Segoe UI",sans-serif;scrollbar-width:thin}' +
// A flex column's children SHRINK by default: inside this scroll box they were squeezed instead of
// scrolled, so the measure toggle collapsed to a 2px line and a caveat card was cut in half (rega).
// scrolled, so the measure toggle collapsed to a 2px line and a caveat card was cut in half.
'.gic > *{flex-shrink:0}' +
'.gic-seg{align-self:flex-start;display:inline-flex;border:1px solid #d1d4cd;border-radius:6px;overflow:hidden;margin-bottom:4px}' +
'.gic-seg button{font:inherit;font-size:12px;padding:4px 12px;min-height:26px;border:0;background:#fff;color:#3b463d;cursor:pointer}' +

View file

@ -116,7 +116,7 @@ useShadowStyles(
'.gic2{grid-area:combos;display:flex;align-items:center;gap:10px;padding:6px 14px;background:#fff;border-bottom:1px solid #d1d4cd;' +
'min-width:0;font:12.5px/1.3 Roboto,-apple-system,"Segoe UI",sans-serif;color:#3b463d}' +
'.gic2-h{flex:none;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:#6d766e}' +
// The selection side is BOUNDED at 40% (rega r82: the placeholder ran under "Try a combination"), but only
// The selection side is BOUNDED at 40% (the placeholder ran under "Try a combination"), but only
// its chips and placeholder clip: they scroll or ellipsize. No overflow here and no min-width:0, so the
// flex:none label sets the minimum width and is never cut ('YOUR S…' at 390, the zh/ru/hi/ar labels at 1280).
'.gic2-sel{display:flex;align-items:center;gap:6px;flex:0 1 auto;max-width:40%}' +

View file

@ -215,7 +215,7 @@ useShadowStyles(
@click="board.openGroup(r.slug)" @keydown="onRowKey($event, r)"
@mouseenter="bus.setHover(r.slug)" @mouseleave="bus.setHover(null)">
<div class="gig-td gig-chk" role="cell" @click.stop>
<!-- a 24 px hit area around the 15 px box (WCAG 2.5.8 target size; rega RS2) -->
<!-- a 24 px hit area around the 15 px box (WCAG 2.5.8 target size) -->
<label class="gig-hit"><input type="checkbox" :checked="isSel(r.slug)" :aria-label="T('gospelInternet.grid.selectRow', 'Select {name}', { name: r.people_group })" @change="bus.toggleSelect(r.slug)" /></label>
</div>
<div v-for="c in visibleCols" :key="c.key" class="gig-td" :class="{ num: c.num, 'gig-sticky': c.sticky }" :style="{ width: c.w + 'px' }" role="cell">

View file

@ -363,7 +363,7 @@ useShadowStyles(
'background:rgba(255,255,255,.96);color:#3b463d;border:1px solid #d1d4cd;border-radius:10px;padding:8px 10px;' +
'font:12px/1.4 Roboto,-apple-system,"Segoe UI",sans-serif;box-shadow:0 2px 10px rgba(0,0,0,.10)}' +
'.gim-lh{display:flex;align-items:center;gap:8px;font-weight:600;font-size:12px;margin-bottom:4px}' +
// a real 28px tap target (WCAG 2.5.8 wants 24): the glyph alone measured 16px (rega r75)
// a real 28px tap target (WCAG 2.5.8 wants 24): the glyph alone measured 16px
'.gim-lh button{margin-inline-start:auto;border:0;background:none;color:#6d766e;cursor:pointer;font:inherit;display:inline-grid;place-items:center;width:28px;height:28px;padding:0;flex:none}' +
'.gim-keys{display:flex;flex-wrap:wrap;gap:3px 10px;margin-bottom:6px}' +
'.gim-key{display:inline-flex;align-items:center;gap:5px;font-size:11px}' +

View file

@ -88,7 +88,7 @@ export const SHARE_FACETS = Object.fromEntries(Object.entries(URL_KEYS))
// MAP STATE: what a link written while the map was the default carries and only the map shows (an open or
// selected people group's pin, a country or province, the province view). Such a link with no ?panel= opens
// the MAP, so old map links keep working; a bare link opens the tree (dx-shepherd ENDTOKEN-GOINT-ATTR).
// the MAP, so old map links keep working; a bare link opens the tree.
const MAP_STATE = ['group', 'groups', 'country', 'province']
export const hasMapState = (q) => MAP_STATE.some((k) => q.get(k)) || q.get('view') === 'province'

View file

@ -73,7 +73,7 @@ export function useBoard() {
const x = readiness.value.byIso.get(r.iso639_3)
// The column RANKS languages WITHOUT a New Testament (the translation question): a language that has
// one scored 5 (its Bible counted as Scripture) and sorted to the top, the opposite of what a
// translation team needs (rega, pre-landing T1). Blank sorts last; the drawer still shows the signals.
// translation team needs. Blank sorts last; the drawer still shows the signals.
r.open_data = x && !x.nt ? x.signals : null
r.open_grammar = x && x.grammar ? x.grammar : ''
r.open_scripture_books = x ? x.scripture_books : null
@ -153,7 +153,7 @@ export function useBoard() {
let pendingTree = ''
// A ?tree= link applies only once BOTH the tree and the rows are loaded, whichever arrives last:
// setTreeNode keeps the node's languages that are on the board, and with no rows yet that is the empty
// set, so the grid restored to 0 (rega F1: "6 of 2,107" before a reload, 0 after).
// set, so the grid restored to 0 ("6 of 2,107" before a reload, 0 after).
function applyPendingTree() {
if (!pendingTree || !tree.value || !all.value.length) return
setTreeNode(pendingTree)
@ -217,7 +217,7 @@ export function useBoard() {
if (!writeUrl.value || typeof history === 'undefined') return
let keep = location.search
// A tree node is shared as ?tree=<glottocode> ALONE: its language list (hundreds of codes for a big
// family) made links that break in chat apps and SMS, the field's channel (dx-shepherd GOFAM). The
// family) made links that break in chat apps and SMS, the field's channel. The
// kit writes `language` from the bus; this writer runs after it (flush:'post') and removes it while a
// node is active. On load, ?tree= restores the node, which re-applies its languages itself.
if (treeNode.value) { const q = new URLSearchParams(keep); q.delete('language'); keep = q.toString() ? '?' + q.toString() : '' }

View file

@ -23,11 +23,10 @@ function setGlobal(name, value) {
* on a light country — that case is the point of the map, not a rendering fault. A
* country Doxa does not work is left uncoloured, not drawn as covered.
*
* > design note 2026-08-01: "mapping out the gospel resources is super important for me
* > … key city posters, cities with internet, gospel resources — all these are
* > missing maps."
* Why this map exists: gospel resources had no map of their own, alongside key city posters
* and cities with internet.
*
* MOBILE FIRST, deliberately. The brief asked for mobile-friendliness before this
* MOBILE FIRST, deliberately. Mobile-friendliness was a requirement before this
* map existed, so it is built that way rather than retrofitted:
* - one WebGL context (the ~16-cap rule; this is the app's only map pane)
* - tap-to-select with a bottom SHEET, not a hover popup — hover does not exist
@ -461,12 +460,11 @@ const cardShard = (slug) => {
const sxScope = (sx) => {
if (!sx) return ''
const n = Number(sx.parts) || 0
/* ⚠ THE WORDS ALREADY EXISTED, TRANSLATED, and I asked for new keys without looking. rega found
gospel.charts.newTestament and gospel.charts.fullBible in all 11 locales — used by the charts
leaf — while my gospel.res.* existed nowhere, so every locale including Arabic fell back to my
English defaults, and even en read "full Bible" against the catalogue's "Full Bible". Two keys
for one word is the drift I keep fixing in other people's code; these are the same words, so
they are the same keys. Only the part count needed a new one. */
/* ⚠ THE WORDS ALREADY EXISTED, TRANSLATED. gospel.charts.newTestament and gospel.charts.fullBible
are in all 11 locales (used by the charts leaf), while the gospel.res.* keys first added here
existed nowhere, so every locale including Arabic fell back to the English defaults, and even en
read "full Bible" against the catalogue's "Full Bible". Two keys for one word is drift; these are
the same words, so they are the same keys. Only the part count needed a new one. */
const name = n === 66 ? t('gospel.charts.fullBible')
: n === 27 ? t('gospel.charts.newTestament')
: n ? t('gospel.res.nParts', { n: fmt(n) })
@ -599,7 +597,7 @@ function openFromUrl() {
const country = (sp.get('country') || '').trim()
/* ⚠ A COUNTRY IS AN ADDRESS TOO, and it was not one: ?country=IND did nothing at all unless a
?code came with it, because country was only ever a tie-breaker between two pins sharing a
language. The brief's second requirement — click a country, see its resources — had no URL,
language. The second requirement — click a country, see its resources — had no URL,
which is precisely what a partner embeds. Accepts the ISO3 or the country's own name. */
if (!code && country) {
const key = country.toUpperCase()
@ -1720,7 +1718,7 @@ function openPin(i) {
* positioned by no stylesheet at all.
*
* ONE IMPLEMENTATION, NOT TWO. This was first fixed with a local copy built on the already-
* exported engineStylesheetUrl, because nothing shared needed changing for it. exp-shepherd
* exported engineStylesheetUrl, because nothing shared needed changing for it. The library
* then exported ensureEngineCSS for the same defect on connectivity-map, so this converges
* onto theirs — and theirs is the better one: it also waits on a link that is PRESENT BUT
* NOT YET LOADED, a case the local copy would have skipped straight past.
@ -1820,7 +1818,7 @@ onMounted(async () => {
style: resolveNamedStyle(isDark.value ? 'ink' : 'paper', grProvider).style
|| (isDark.value ? 'mapbox://styles/mapbox/dark-v11' : 'mapbox://styles/mapbox/light-v11'),
accessToken: mapboxToken.value,
/* THE ENGINE'S OWN STRINGS, IN THE READER'S LANGUAGE (rega row 34). This leaf calls
/* THE ENGINE'S OWN STRINGS, IN THE READER'S LANGUAGE. This leaf calls
`new gl.Map` directly instead of going through useMapInstance, so it did not inherit
the locale map the shared composable passes — "Toggle attribution" and the canvas's
accessible name "Map" stayed English under ar while every string we own was Arabic.
@ -1845,7 +1843,7 @@ onMounted(async () => {
/**
* ═══ THE CREDIT MUST NAME THE BASEMAP THAT IS ACTUALLY DRAWN ═════════════════════════
*
* REPORTED by rega: under ?provider=maplibre this line read "geoBoundaries · Mapbox ·
* Under ?provider=maplibre this line used to read "geoBoundaries · Mapbox ·
* © OpenStreetMap" while the tiles came from OpenFreeMap. Crediting the wrong supplier is
* worse than crediting none — it is a false statement about whose work is on screen, and
* OpenFreeMap's terms ask for their credit.
@ -1869,7 +1867,7 @@ onMounted(async () => {
/**
* ═══ 'load' MAY HAVE ALREADY FIRED ════════════════════════════════════════════════════
*
* ⚠ THE DEFECT THIS WAS WRITTEN FOR WAS RETRACTED. rega first reported that under
* ⚠ THE DEFECT THIS WAS WRITTEN FOR WAS RETRACTED. A first reading said that under
* ?provider=maplibre this map drew the basemap and nothing else; re-measured by polling
* until the tiles and features were actually in, it draws 170 fills and 2,107 pins on both
* engines. The original reading was taken on a fixed 18 s timer, right as the layers
@ -2085,7 +2083,7 @@ useShadowStyles(
'.gm-linklist span{color:#6d766e}' +
'.is-dark .gm-linklist{background:rgba(59,70,61,.6);border-color:#6d766e;color:#f3f3f1}' +
'.is-dark .gm-linklist span{color:#d1d4cd}' +
/* ⚠ LOGICAL, NOT PHYSICAL (found in use, rega measured on main 52eeb0db). Under ar the five
/* ⚠ LOGICAL, NOT PHYSICAL (found in use, measured on main 52eeb0db). Under ar the five
map controls mirror to the LEFT while `left:8px` kept the legend physically left, so the
legend sat underneath all five of them. cities-internet shares this class name and passed
for one reason: it anchors inline. Every physical left/right in this stylesheet that has a
@ -2510,7 +2508,7 @@ useShadowStyles(
language. -->
<!-- ⚠ THIS SENTENCE IS NOT PROSE AND MUST NOT STAY ENGLISH. It is the legend's one
piece of news — that almost every group with nothing has somewhere to start — and
rega measured it as the Latin left in an Arabic legend once the explainer was
it measured as the Latin left in an Arabic legend once the explainer was
folded away. Through t() with its numbers as parameters, so it renders the catalogue's
English until the key lands and then switches. The CAVEAT that followed it ("a way
in is not the gospel in their own language") moved into the fold below, where the
@ -2625,7 +2623,7 @@ useShadowStyles(
A group is placed in a province from its own coordinate, which marks where a people is
found rather than a settlement. Near a border that point can fall on the wrong side.
</p>
<!-- The brief's THIRD requirement at the province grain: clicking a place filters the
<!-- The THIRD requirement at the province grain: clicking a place filters the
table to it. Same split as the country and language links — a filter when the tables
are already beneath this map, a link when they are not. -->
<button v-if="composed" type="button" class="gm-tablink"
@ -3320,7 +3318,7 @@ useShadowStyles(
</a>
<!-- Same split as the language link above: a filter when the tables are already on
screen beneath this map, a link when they are not. This is the brief's SECOND
screen beneath this map, a link when they are not. This is the SECOND
requirement — click a country, see its resources — and in the composite it was
answering by leaving the composite. -->
<button v-if="selected.iso3 && composed" type="button" class="gm-tablink"

View file

@ -117,7 +117,7 @@ for (const r of roster) {
},
/* ⚠ EVERY CHIP READ "unchecked" IN CARD MODE, and that understates the work: these links ARE
verified, but the state lives in gospel-link-verification.json which the card never fetches.
rega caught it on central-sama, whose Scripture and film are both verified. The states for
It showed on central-sama, whose Scripture and film are both verified. The states for
THIS group's own URLs ship with it — the state only, never a re-derivation — keyed by URL
exactly as linkVerify is, so the sheet's existing vchip()/vtitle() work unchanged. */
verified: (() => {

View file

@ -400,7 +400,7 @@ const NOT_SCANNED = 'api_bible, dbl, progress_bible, youversion'
// gap match above is a name match and is worth nothing on its own.
// (3) DOCTRINE UNVERIFIED, not suspected. No statement of faith is served
// at /about.php, /aboutus.php, /faith.php or /beliefs.php — all 404.
// The brief's rule is to leave a source out when its doctrine is
// The rule is to leave a source out when its doctrine is
// unclear and say why, so that is what this is. It is NOT a finding
// against them; it is the absence of a finding either way.
// - IBT (Institute for Bible Translation, ibtrussia.org) — NOT a pool, but the best LEAD
@ -761,7 +761,7 @@ const tierOfCode = (c) => {
Measured on this corpus: 602 codes and 2,001 of 12,379 groups move 1 -> 2. Tier 0 does not
move at all (1,592 before and after), so nothing about who has NOTHING changes — the gap
lists, the 369 zero-access groups and the bridge-reach table are untouched by this.
Found from go-int's cross-check of 20 languages with "no NT in the pools": 13 of the 20 had
Found by a cross-check of 20 languages with "no NT in the pools": 13 of the 20 had
readable text all along, on a record this function could not see. */
const hasS = hasReadableScripture(c), hasF = !!c.f
const hasD = !!(c.d && c.d.length)

View file

@ -1,7 +1,7 @@
/**
* fetch-ebible-credits.mjs — WHO translated the Scripture this map links to.
*
* WHY. The brief asks for real leads: real translators and the real organisations that do the
* WHY. The aim is real leads: real translators and the real organisations that do the
* work, not just a URL. A mobiliser who finds a neighbouring language already has Scripture
* wants to know who produced it, because that organisation is the one with the linguists, the
* relationships and very often a field team already in that country. The link answers "where";

View file

@ -2,7 +2,7 @@
/**
* verify-gospel-links.mjs — fetch EVERY link this map ships and record whether it resolves.
*
* The brief's rule is "verify a URL resolves before shipping it". This is the script that
* The rule is that a URL is verified to resolve before it ships. This is the script that
* discharges it, and it now checks the corpus the map actually holds rather than the one it
* held in August.
*

View file

@ -13,9 +13,9 @@
* here, where every entry was screened by hand, or it does not ship at all.
*
* ⚠ WHAT IS DELIBERATELY ABSENT. National Bible Societies hold 89+ of the credits and NONE of
* them is linked here. Not doubt about the Societies — the brief's filter is "only the 66-book
* canon", and several UBS members publish editions that include the deuterocanonicals, so
* whether a Society CONTACT is inside or outside that line is a product ruling to make and not
* them is linked here. Not doubt about the Societies — the filter is the 66-book canon only,
* and several UBS members publish editions that include the deuterocanonicals, so
* whether a Society CONTACT is inside or outside that line is a product decision to make and not
* this script's. They stay named-without-a-link until it is made, and the count is reported so the
* gap is visible rather than quiet. Same for the orgs whose doctrine I could not establish: they
* are listed in `unscreened` with the reason, never linked.