- useMapTheme: one look rule (any prayer strategy → night/default; everything else →
day/monochrome) applied at boot and on every tab switch in the simple map and the
research map; an unknown strategy keeps the current look
- Legends and the research search pill are 380px again; desktop numeric columns 70px at
13px so every row stays on one line with no overlap
- Map containers carry their map instance for gates (no global)
- data/ tables and the people-groups file: meta blocks reduced to provenance (source, url,
downloaded, rows, grain, attribution); the link-verification table keeps only state and detail
per link; generators write the same shape
- dropin.exclude in the build config: people_groups.csv, open-doors.json, platform-reachability.json
and tables/index.json are not shipped (nothing loads them); sync-dropin honours it
- public-hygiene --strict flags note-style meta keys in shipped data
- Rebuilt bundles and gallery
- internal/playground → internal/plugin-gallery; playground-smoke → plugin-gallery-smoke;
generate-spa → generate-gallery (scripts: gallery, smoke:gallery; smoke:playground stays as alias)
- generate-gallery exports renderGallery(model, { mode }) — the dev plugin serves the same page
at / from the discovered registry (dev mode imports the dev entries, ladder popovers on);
the build writes public/index.html from the manifest (byte-identical to before)
- Docs and probes use the gallery vocabulary
- index.html is generated by internal/generate-spa.mjs from doxa-maps/manifest.json: one
self-contained page that lists every bundle, profile and instance, mounts the chosen one,
and offers Open page / Copy embed; build ends with `bun run spa` (the material shell is no
longer installed; nav.js, icons.js, languages.js, pane-wrapper/, external/ and _headers
leave public/)
- The website drop-in follows the same contract: dropin.include in the build config,
sync-dropin --from-config --prune, public-hygiene --strict fails on any other root entry
- Production bundles no longer carry CSS comments: a build-only Vite plugin strips them from
every shadow-style block (about 140 per map bundle, 5% smaller); remaining comments reworded
- Share pages and the pin renderer drop their note-style comments
- SemanticTreeLegend: the UUPGs/Population columns were a fixed 44px, sized for the old
9px header / 10.5px numbers; at the desktop scale "POPULATION" collided with "UUPGs"
and "139.0M" spilled into the neighbouring column. Base column is now 60px, desktop
76px (header 10px, numbers 13.5px, no wrapping)
- The panel follows --map-legend-width like the detail card and the geocoder pill; the
research map's wide-host legend is 400px so "Full Prayer Coverage" stays on one line
- New probe: `bun run probe:legend-columns` measures cell boxes vs real text extents and
fails on any spill or overlap (desktop and phone sheet)
- New shared LegendProgressBar: [has-prayer span | needs-prayer remainder] with the
full-coverage colour filling inside the has-prayer span; text "covered of total · %";
each span is a focusable button with the legend's info popover (name · count · %),
hover highlight, and click selects the matching legend row
- useLegendData exposes a `progress` payload for the prayer legend (mutually exclusive
counts from /statistics: covered = has + full); null for every other legend
- SemanticTreeLegend and LegendRows render the bar as the header's second line and title
the prayer legend "Progress" (desktop, mobile sheet, collapsed pill); profiles pass and
provide the payload so the mobile sheet gets it unchanged
- legend.progress.* strings in all 11 locales
- No height transition while dragging; the resize grab zone is the top 44px (and, in detail
mode, the content when it is scrolled to the top); one geometry source for the JS clamp and
the CSS max-height; release snaps with hysteresis (open ↔ fullyOpen ↔ collapsed)
- The sheet publishes its height as --legend-sheet-h on the element host; the Mapbox/MapLibre
bottom controls translate up by it, so the logo and attribution stay visible in every state
- New gate: internal/mobile-sheet-probe.mjs (Firefox, touch) taps a pin, drags the detail
sheet up/down and checks heights, states and control overlaps
- useMapTheme: basemapConfig() is the single source of truth; bootConfig() hands Mapbox the
Standard-style config at construction so the very first frame is already night; keepPinned
re-asserts on style.load and styledata, writing only on drift; MapLibre maps are skipped
- useMapInstance passes that config to new Map() and stores the map in a shallowRef so
Mapbox internals never run through Vue's reactive proxy
- Simple map's loading cover is dark like the research map's — the last light frame at boot
- New gate: internal/theme-flash-probe.mjs samples canvas luminance through boot, legend rows
and tabs and fails on any light frame (21 phases, 0 flashes)
- The Mapbox Map instance is unwrapped (markRaw) at the profile boundary and the layers
composable works on the raw map, so Mapbox internals no longer pay Vue's get/set traps on
every frame; the geocoder control instance is a shallowRef for the same reason
- Prayer glow rings animate at 12 steps/s with paint transitions disabled, constants moved to
addLayer, and the loop pauses while the map is off-screen — the map can idle again
- New gate: internal/long-frames-probe.mjs measures long animation frames, blocking time and
DevTools rAF violations per phase (boot / idle / interaction); idle violations on the prayer
and research maps dropped from ~100 to the no-animation baseline
- ShareButton resolves its link from where the bundle is hosted (profile-config.shareUrl ->
embedBaseUrl -> window.MAP_EMBED_BASE_URL -> the bundle's own script URL); profiles pass
tree-relative embed pages, so links work in the playground, on doxa.life and on any CDN
- Generated leaf pages try token endpoints in order (override -> configured absolute URL ->
same-origin) and only then offer the paste-once box; build config points at the canonical
https://doxa.life/api/maps/token
- Dev playground extracted from vite.config.js into internal/playground/{discover,render,dev-plugin}.mjs;
serves /assets/* like the built site so data-driven maps initialise; passes assetsBase +
shareUrl to every element; header ladder with a Start-here primer and the five kinds of
mounting files (single-element, preset-element, multi-view-element, composite-element, app-shell)
- New gates: smoke:playground (every element boots, with status text + data diagnostics),
smoke:share (Share URL resolves and reopens the same map), sync:dropin (mirror public/ into a
host checkout, never credentials), deploy:dev (build + sync)
- MapToolbar + ShareButton on every map profile; UPG-100 composite (map + prayer lists) on the
pane layout; shared --map-radius token consumed by the legend components
- docs/reference.md: profile types table, profile-config assetsBase/shareUrl/embedBaseUrl, gates
An API record with rop1:null passed 'typeof x === object' (null is 'object' in
JS) and read null.label — killing the component on the first semantic tab and
leaving every later tab dead. Null-guarded all three extractors in the affinity
tree builder; SemanticTreeLegend additionally prunes null nodes at its prop
boundary (defense in depth, warns on data holes). Headless verification:
Prayer/Engagement/People Groups/Regions/Languages/Religions all render with
live canvases.
- Compile-time gate __DOXA_QA__ (default off): the QA corner and every string
it carries are dead-code-eliminated from public bundles; platform builds
opt in with DOXA_QA=1. Shell QA becomes ?qa=1 opt-in. Audit: zero platform
strings across all bundles + shell.
- One Mapbox wordmark per page (first live map claims it; released on destroy).
- Coach-mark points at the need: 'Tap a red dot — someone still needs prayer.'
- Pray-click glow: locally-prayed groups render as Has Prayer for that visitor
(localStorage, 7-day TTL) — lasting glow comes from the real signup via the API.
- Staging forge gains 'copy embed': snippet built at click time from
location.href, so it carries the FULL URL of whatever host serves the tree.
data-private/ is born: datasets whose publication could help close doors stay
out of the web-root and out of public repos, permanently. field-access-visas
moved there; the tables index no longer lists it; the key-cities Field Access
tab is removed from the public build; the table generator routes it (and any
future PRIVATE_TABLES entry) to data-private/ and excludes it from the index.
Public-good data (internet gaps, gospel-resource gaps, people-group pairings)
ships as before.
The page URL's ?utm_source= now outranks config/global/default in every outbound
people-group link — an iframe embedded as <page>?utm_source=agwm, a partner page
carrying its own ?utm_source, or a host page like /embed/map/?utm_source=agwm
all attribute their traffic automatically. Applied in the shared popup and both
dashboard drawer links.