- 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
- 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.
The one bundler script that generates all the map pages now stamps
?utm_source=YOUR_SOURCE on every copy-paste iframe snippet (profiles AND
instances) and utmSource:"YOUR_SOURCE" in every script snippet — the same
placeholder pattern as YOUR_MAPBOX_TOKEN. A partner replaces it once and every
people-group click-through from their embed attributes to them.
A profile/instance whose config says provider:'maplibre' now generates a page
that loads the MapLibre peer (jsdelivr, same versions as useMapInstance) and
mounts IMMEDIATELY — the token ladder is skipped entirely. Mapbox pages are
unchanged. demo-map (the example vibe coders copy) is now provider-aware via
resolveMapProvider, and ships a 'maplibre' instance as the living token-free
demo. embed-smoke covers instance pages; verified 2/2 with live canvases —
including the maplibre one with NO token anywhere.
- The build now emits public/_headers implementing the embeddability split
(direct-embed pages framable anywhere; staging preview-only) — Netlify/CF
Pages consume it automatically; serving/traefik-embed-headers.yaml is the
proxy-side reference. The contract is auditable in-repo, not just documented.
- README: docs table gains how-to, layouts/README (the SPA machinery), and
docs/architecture; Quickstart gains bun run serve + bun run phone.
New permanent harness (bun run smoke:embeds): loads every profile page from the
partner-facing manifest headless (SwiftShader software WebGL — no GPU needed),
asserts the custom element defines + populates its shadow DOM, no ProfileLoader
error card, and a WebGL canvas on map profiles. First full run: 17/17 pass —
8 map profiles paint a canvas, 9 DOM profiles (lists/tables/charts/walls)
populate. Exit 1 on any failure, so CI can gate on it.
posters/ (42M webp set the poster walls fetch) and tables/ (the merged served
table set — its index.json is the merge; per-profile data/tables remain the
build-time sources) now live in data/ with build copy-steps. Byte parity
verified; served paths unchanged. public/ is now FULLY regenerable — every
source-of-truth folder has a home outside the web-root.
public/ was confusing because three source-of-truth folders lived ONLY inside it.
They now live in data/ (people-groups, vendor, external-bundles) and the build
copies them into the served tree (same pattern as boundaries), so served paths
are unchanged and public/ is fully regenerable. data/README.md states the law.
- README rewritten as a BLUF-first vision cast for vibe coders (key-terms glossary,
origin story, features, requirements, data caveat, developer's note).
- library/components/WARNING.md: prototype in your app-profile; only proven,
parameterized components graduate into the shared library.
- generate-tree.mjs: inject Subresource Integrity (sha384) + crossorigin into the
standalone page <script> tags and the copy-paste embed snippets (manifest = registry).
- Scrubbed identity-tied phrasing from comments/docs, keeping the technical rationale
and the mission content.
generate-tree now hashes each built bundle IIFE and writes integrity: 'sha384-…' into
manifest.json, so the app-shell can load embedded (direct-mounted) plugins with
integrity=… and refuse a tampered/CDN-swapped bundle. Pairs with the mount/locked
flags convert.py adds (DOXA=embedded, foreign=iframe, all locked).
The SPA loaded mapbox-gl.js + geocoder as render-blocking <script> in <head> (~1.2MB
blocking parse) with no preconnect/preload — the ~10s cold load. Now: defer both (the
app module runs after them in doc order, so window.mapboxgl is still ready at mount),
preconnect api.mapbox.com + pray.doxa.life, and modulepreload the default profile
bundle so mapbox+bundle+API fetch in parallel. Measured headless: time-to-chrome 562ms,
time-to-widget 1068ms (was ~10s). Embed pages get preconnect too (kept mapbox sync there
since they use inline, non-module scripts).
generate-tree now honors a qaWidget build flag (default true) and merges qa:true
into each generated embed page's profile-config, so standalone map plugins opened
in go-base show the QA corner widget too — not just the SPA. Partner embeds use the
copy-embed snippet with their own config, so they are unaffected. Opt out with
qaWidget:false in vite.doxa-maps-build-config.json.
Add a gatedProfiles config field (default [key-cities]) honored by the tree
generator: a gated bundle's .js still BUILDS via vite, but it never appears in
the staging root, the per-bundle staging pages, or manifest.json — the surfaces
generate-tree emits. This makes key-cities gated in ONE place for every surface
(convert.py already gates the go-base plugin index), instead of relying on the
box-side converter alone. The staging root now shows all 9 bundles + their
elements with no key-cities leak.
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.
History reset to a single clean commit; no PII, IPs, or internal terms.