doxa-maps/docs/reference.md
swift-river542 a876e5e97b Host-agnostic share links, token-endpoint chain, dev playground as reusable modules, share/asset gates, validated profile-type vocabulary
- 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
2026-09-16 12:10:38 -05:00

21 KiB

Reference

Look-up tables and contracts for doxa-maps. For learning, see the tutorial; for tasks, how-to.md; for the why, why.md; for the short "what it is", explanation.md.

doxa-maps is a Vite bundler: one shared library, N independent embeddable bundles. Each bundle is a single self-contained .js file that registers a custom HTML element (<doxa-map>, <doxa-research-map>, …) you can drop on any web page.


The three zones

Zone Path Alias License You edit it…
Shared library library/ @map Apache-2.0 …once; the fix reaches every bundle on next build
Your app-profile app-profiles/<name>/ — yours …freely; nothing here leaks to other maps
The build machine internal/ — Apache-2.0 …never — it's the bundler's own scripts

Terminology. "Profile" is used at two levels: a bundle / app-profile is a folder under app-profiles/; a profile screen is one .vue file sitting flat in that folder (no profiles/ subfolder — the entry globs ./*.vue). A bundle can hold several screens; the host picks which to render with the profile field of profile-config.

Two words first. An element is a custom HTML tag this repo invents, such as <doxa-map>: the browser learns it once, then you can paste it into any page as often as you like; each pasted tag is one live copy, an instance (people also say "web component"). An app profile (this repo also says mounting file) is one flat .vue in app-profiles/<bundle>/ that defines a whole map app — its main component; the settings on the tag (profile-config) name which app profile to load and the shared ProfileLoader picks it. In class-and-object terms: the element class is the class, each tag is an instance, the app profile is the screen that instance loads. Most repos need one such file per app; doxa-maps builds hundreds of maps from one shared library/, so every map gets its own app profile — and those files come in five shapes:

Profile types — the ladder, smallest first (head words are established terms; the -element compounds are this repo's own; the coined names stay as a.k.a.): single-element → preset-element → multi-view-element → composite-element → app-shell.

Type (a.k.a.) Machine key What it is How the build recognises it
single-element (single) single one app profile, one map — paste its tag anywhere, set it up in profile-config; the default a flat .vue with no <profile>.meta.json and no *.instances.json in the folder
preset-element (parameterized) parameterized one app profile + a small table of named presets; each row is a ready-made map with its settings baked in, with its own embed page and sidebar entry — same code, same tag the bundle ships <name>.instances.json — { "instances": [{ "id", "title", "props" }] } or a bare array; the build writes one embed page per row at <bundle>/<profile>/<id>/index.html
multi-view-element (monolithic) monolithic one app profile holding several screens you switch between — tabs or steps — in one .vue, shown one screen at a time declared: <profile>.meta.json → { "type": "monolithic" } (or "multi-view-element")
composite-element (nested-elements-mfe) nested-elements-mfe an app profile whose template places other app profiles' maps together (stacked or side by side, e.g. a map + a poster wall) and ships the set as one new element — one tag embeds it all declared: <profile>.meta.json → { "type": "nested-elements-mfe", "composes": [ … ] } (or "composite-element")
app-shell (nested-mfe-spa) nested-mfe-spa the outer frame — sidebar, header, navigation — that loads any of the other kinds and switches between them without a reload; the only rung that is a page, not an element today an HTML page (layouts/app-wrapper-material/index.html, the dev playground); the key is reserved for a future .vue shell

Composite kinds are always declared in <profile>.meta.json ({ "type": …, "label": … }, either spelling — see TYPE_ALIASES in internal/playground/discover.mjs) — the build never guesses them from source (tag-scanning produced false positives). Discovery is one pure module, internal/playground/discover.mjs; bun run dev renders its result as the auto-generated SPA (sidebar grouped by bundle, most-composed first, each element deep-linkable at #/<bundle>/<id>), and bun run smoke:playground proves every element actually boots inside it.


Directory tree

doxa-maps/
├── library/                      ← SHARED LIBRARY (alias @map, Apache-2.0)
│   ├── index.js                  ← thin barrel; prefer subpath imports for tree-shaking
│   ├── ProfileLoader.vue         ← the ONE component that reads profile-config & loads a screen
│   ├── components/               ← map UI: legends/, map-controls/, poster/, dashboards/, drawers
│   ├── composables/              ← useMapInstance, useMapData, useMapLayers, … (+ legends/, poster/)
│   ├── api/                      ← DataSourceManager (csv | api | rest-api) + sources.json
│   ├── colors/                   ← one color strategy per file, auto-registered by _registry.js
│   ├── constants/                ← never-change values: mapDefaults.js, zoom.js, poster*.js
│   ├── i18n/locales/             ← 11 base locales: ar de en es fr hi it pt ro ru zh
│   ├── stores/                   ← Pinia: dataStore, mapStore, uiStore (per-instance)
│   └── utils/                    ← pure helpers (geoUtils, clustering)
│
├── app-profiles/                 ← ALL bundles live here (each subfolder = 1 bundle)
│   ├── <bundle>/
│   │   ├── index.js              ← bundle ENTRY — globs ./*.vue, registers the web component
│   │   ├── index.html            ← Vite html entry = the dev STAGING page
│   │   ├── <profile>.vue         ← 1..N FLAT profile screens (no profiles/ subfolder)
│   │   └── src/                  ← everything else the map needs (its own colors/, api/, …)
│   └── template-bundle/          ← minimal copy-me reference bundle
│                                    (folders starting with _ or . are SKIPPED by the build)
│
├── internal/                     ← the bundler's own build scripts — never edited by map builders
├── layouts/                      ← the app shell: mounts several bundles as one single-page app
├── capacitor/                    ← native projects (android/, ios/)
├── public/                       ← THE SERVED WEB-ROOT, committed:
│   ├── index.html                    the app shell
│   ├── doxa-maps/<bundle>/           built bundles + manifest.json + staging HTML
│   └── assets/                       posters & data the maps fetch at runtime
├── vite.config.js                ← multi-entry: discovers app-profiles/<name>/ folders
└── vite.doxa-maps-build-config.json ← output tree name + copy destinations + token settings

The profile-config runtime contract

A bundle exposes ONE attribute, profile-config — a JSON string parsed by ProfileLoader.vue at runtime. It is the entire wiring between host page and map, so the same built bundle re-points to different data/colors/language with no rebuild.

<doxa-map profile-config='{
  "profile":    "doxa-simple-map",   // which flat .vue screen to render (required)
  "tk":         "pk.eyJ...",         // Mapbox token (required; host-supplied, never baked)
  "instanceId": "my-map",            // optional; isolates window events between embeds
  "dataSource": "pray-tools",        // or { "type":"rest-api","endpoint":"https://…" }
  "colorSet":   "default",
  "locale":     "en",
  "provider":   "mapbox",           // map engine: "mapbox" (default) | "maplibre" (preview);
                                     //   also flippable per page via ?provider= or window.MAP_PROVIDER
  "assetsBase": "/assets",           // where tables/, boundaries/, previews/ are served (see below)
  "shareUrl":   "https://…",         // optional; the WHOLE url the Share button links/embeds (beats everything)
  "embedBaseUrl": "https://…/doxa-maps", // optional; served tree root the Share button joins embed pages to
  "tabs":       [ /* parameterized screens read this */ ]
}'></doxa-map>
  • assetsBase — data-driven profiles (connectivity-map, resources-map, cities-map, the poster walls and data tables) fetch ${assetsBase}/tables/*.json and ${assetsBase}/boundaries/*.geojson before they create their map. Unset, it defaults to ./assets relative to the host page; if the host doesn't serve that path the fetch gets an HTML fallback, JSON.parse throws, and the map is never created (no console error, only the profile's status line). The generated pages and the shell set it for you (../../../assets or the absolute assetsBaseUrl from the build config); any other host must point it at a copy of public/assets/.

  • Share ladder (library/utils/embedBase.js) — what the Share button links and iframes: shareUrl (a whole url, e.g. the playground's own deep link) > the profile's tree-relative embed page (<bundle>/<profile>/index.html, the embedPath prop or shareEmbedPath) joined to an embed base — embedBaseUrl > window.MAP_EMBED_BASE_URL > the root the built bundle derives from its own script url (two segments above <bundle>/<bundle>.js, so a dropped tree works on any host/CDN with no config) > the page's own location. Never hardcode /js/doxa-maps/… in a profile.

  • profile must match a flat .vue filename in the bundle. Mismatch → an inline "Profile not found" error box listing the available names.

  • tk is supplied at runtime so the token is never compiled in (dev uses .env's VITE_MAPBOX_TOKEN).

  • instanceId scopes cross-instance window events so multiple embeds never interfere.

  • dataSource is handed to DataSourceManager (csv | api | rest-api).

  • ProfileLoader provide()s to descendants: mapboxToken, dataSource (both computed refs — unref before use), colorSet, profileConfig, instanceId, lang, and the instance-scoped Pinia stores uiStore / mapStore / dataStore. Descendants inject; never call useXxxStore() directly (it would cross-bleed between embeds).

Each bundle registers a shadow-DOM web component via defineCustomElement, with its own ShadowRoot, styles, Pinia, and i18n. customElements.define() is one-shot per tag name, so each bundle registers a distinct tag.


Build scripts & output

Script What it does
bun run dev Vite dev server + HMR (default http://localhost:5173, or the next free port — read the terminal). / is the auto-generated SPA (playground) of every element, built from internal/playground/{discover,render,dev-plugin}.mjs; every bundle is also served at its own html path. Dev-only routes: /__playground/registry.json (the registry), /assets/* (served from public/assets/, else app-profiles/*/data/). Writes nothing to public/ — no HMR across that boundary.
bun run smoke:playground Opens every element of the auto-generated SPA by deep link in Firefox and proves it boots: element defined, shadow DOM populated, no error card, a WebGL canvas for map profiles (checked through nested shadow roots), no HTML-for-JSON data responses. --json <file> writes the evidence; exit 1 on any failure. The pre-handoff gate.
bun run smoke:share The share round-trip gate (Firefox): opens a host page (--playground = every canvas element of the dev SPA; --url <page> --tag <element> = the website or any CDN page), clicks Share, reads the URL, fetches it, reopens it in a fresh page and asserts the SAME custom element mounts with a WebGL canvas. --json <file>; exit 1 on any failure — proves REQ-1 ("links work wherever the tree is hosted").
bun run sync:dropin <dest> Mirrors the built web-root public/ into a host checkout (e.g. the website's public/js/doxa-maps-build/): adds, updates and deletes to match; never copies credentials (api/maps/token.json, token.local.js); protects host-owned files (README.md); --dry-run first. The only sanctioned way a map changes on the site: change the bundler → bun run build → sync → commit in the host.
bun run build clean → one vite build per app-profiles/*/ folder → tree → check:drop → check:parse.
bun run build:bundle Builds ONE bundle (needs BUNDLE=<name>).
bun run build:watch Rebuilds into public/ on every change (integration loop).
bun run clean Wipes + recreates the public/<name>/ output tree.
bun run tree Writes manifest.json + self-contained staging HTML, then copies the tree to every build-config paths destination.
bun run verify Data-integrity + shipped-bundle + network-policy guards (fast, no browser).
bun run verify:full + rendered posters and live apps in Firefox.

How a build works. vite.config.js is multi-entry by discovery: at build time it registers every app-profiles/ subfolder that (a) doesn't start with _/. and (b) has both index.html and index.js. One vite build builds exactly one bundle, selected by BUNDLE=<name>, so the output is a self-contained IIFE (inlineDynamicImports: true). bun run build loops every folder. Each bundle only bundles what its screens import, so sizes differ (simple ≈ 420 KB, research ≈ 1.3 MB).

Where output goes. One bundle folder → one file: public/doxa-maps/<bundle>/<bundle>.js. CSS is injected by JS (cssCodeSplit: false), so there is no separate .css. The tree root name comes from vite.doxa-maps-build-config.json name (default doxa-maps). emptyOutDir: false keeps sibling bundles intact when you rebuild one.

What a built bundle folder contains. Beyond the IIFE, bun run tree generates an index.html per bundle (staging/showcase), per profile, and per instance — each profile/instance page is a standalone, directly linkable/iframe-able map — plus public/doxa-maps/manifest.json with ready-made embed.iframe/embed.script snippets for every profile. Full design: architecture/embeddability.md.

Preset instances. A <profile>.instances.json beside a parameterized profile ({ profile, apiBaseUrl?, instances: [{ id, title?, props }] }) makes the build emit one standalone page per instance at <bundle>/<id>/index.html. Recipe: how-to.md.

Bun is required. bun.lock is the committed lockfile; bun x vite runs project-local Vite. npm run … resolves the same scripts but Bun must be on PATH.


vite.doxa-maps-build-config.json fields

Edit this one file, then bun run build.

Field What it does
name Output folder name (default doxa-maps).
paths Extra folders to copy the whole built tree into. Point at a host repo's public/ → that host serves the maps and becomes the CDN. Empty (the default) ⇒ written to public/<name>/ only, since public/ is already the committed web-root.
embedBaseUrl The public URL the tree is served from, used to generate absolute, paste-anywhere embed snippets. Empty ⇒ relative URLs (position-independent; keeps the drop-portability gate green; required for Capacitor, which loads from https://localhost).
tokenUrl Endpoint each generated page fetches its Mapbox token from — a URL, not a secret. Set absolute (https://…/api/maps/token) so a CDN folder and the mobile app both work; endpoint must send CORS for the embedding origins. window.MAP_TOKEN_URL overrides at runtime.
token Opt-in literal token baked into every page. Leave empty — prefer tokenUrl.
assetsBaseUrl Where the shared asset tree (previews, posters, boundaries) is served from. Empty ⇒ a relative path per page depth (self-contained under one web-root). Set absolute for partner embeds and mobile, where there is no shared web-root to climb to.

Token resolution ladder: per-map tk prop > ?tk= > baked token > window.MAP_TOKEN_URL > tokenUrl > same-origin /api/maps/token.


Data sources

Maps read data through @map/api/ driven by library/api/sources.json. Three source types:

Type What Points at
rest-api versioned REST endpoint (default) the live people-group API; base URL resolves at runtime (window.MAP_APP_API_URL → VITE_API_BASE_URL → same-origin)
csv a local CSV parsed at load the Joshua Project dataset (+ photo fallbacks)
api a single JSON URL any plain JSON endpoint

Country/region polygons are not a sources.json connection — they render from Mapbox's built-in country-boundaries-v1 tileset plus a lazy-loaded WAGF-regions GeoJSON.

Adding or changing a source? Full contract + checklist: architecture/data-sources.md.


Colors

Reused by more than one map → shared library/colors/. Only your map uses it → your bundle's src/colors/ (merges over the shared set, that bundle only). Filename derives the mode key (language-family.js → languageFamily).

Writing or overriding a strategy? Full contract + checklist: architecture/color-strategies.md · two homes in depth: ../contributing/COLORS.md.


Importing from @map

Everything under library/ is importable via the @map/… alias (vite.config.js maps @map → library/). The import path mirrors the file path: a file at library/<sub>/<file> imports as @map/<sub>/<file>. Examples:

import SemanticTreeLegend from '@map/components/legends/SemanticTreeLegend.vue'
import { useMapInstance }  from '@map/composables/useMapInstance.js'
import { useMapData }      from '@map/composables/useMapData.js'
import langFamilyByIso     from '@map/data/langFamilyByIso.json'

Prefer subpath imports over the thin @map barrel (which exports MAP_CORE_VERSION only) so tree-shaking works. The per-file catalog in architecture/file-map.md is the authoritative menu — every library/ file listed is importable this way. A generated one-line import index can also be produced with node internal/generate-library-index.mjs.


Going deeper — the architecture folder

Simple lookups live here; the full contracts, catalogs, and wiring detail live in architecture/:

Doc What it holds
file-map.md every file, one line each, with the gotcha each carries
data-sources.md the api/ seam: connection contract + checklist
color-strategies.md the strategy contract + legend wiring
components.md shared component catalog + conventions
i18n.md the locale contract + add-a-language checklist
embeddability.md generated per-map pages + manifest — embeddable by default
app-wrappers.md the SPA shell: nav.js + PROFILES contracts
poster-generation.md the poster/preview generator pipeline
patterns-and-provenance.md the two multi-map patterns + provenance

The Semantic Tree Legend component (a reusable hierarchical legend) is documented in file-map.md.


Quick reference — "I want to…"

I want to… Do this
Add a new map copy app-profiles/template-bundle/ → rename folder + tag in index.js
Add a screen to an existing bundle drop a .vue flat in that bundle's folder, request it via profile-config.profile
Fix a legend / control for ALL maps edit it once in library/ (@map); rebuild bundles
Add a color strategy new file in @map/colors/ (shared) or the bundle's src/colors/ (map-private)
Build everything bun run build → public/doxa-maps/<bundle>/<bundle>.js
Build just one BUNDLE=<name> bun run build:bundle
Preview in isolation bun run dev → http://localhost:5173/app-profiles/<name>/index.html
See a change on the served site bun run build, then refresh (NOT dev)
Repoint the API without a rebuild set window.MAP_APP_API_URL before the bundle loads
Repoint the Mapbox token pass tk in profile-config, or set window.MAP_TOKEN_URL / build-config tokenUrl